对这个单元测试感到困惑!

发布于 2024-10-01 17:23:52 字数 1168 浏览 2 评论 0原文

基本上,我有一个抽象类,它有一个唯一的增量 ID - Primitive。当实例化 Primitive (或更准确地说,Primitive 的继承者)时,ID 会递增 - 直到 ID 溢出的点 - 在这一点上,我添加异常消息并重新抛出。

好的,一切正常......但我正在尝试测试这个功能,而且我以前从未使用过模拟。我只需要制作足够的原语以使 ID 溢出并断言它在正确的时间抛出。

  • 实例化 20 亿个对象来做到这一点是不合理的!但是我没有看到其他方法。
  • 我不知道我是否正确使用了模拟? (我正在使用Moq。)

这是我的测试(xUnit):

[Fact(DisplayName = "Test Primitive count limit")]
public void TestPrimitiveCountLimit()
{
    Assert.Throws(typeof(OverflowException), delegate()
    {
        for (; ; )
        {
            var mock = new Mock<Primitive>();
        }
    });
}

并且:

public abstract class Primitive
{
    internal int Id { get; private set; }
    private static int? _previousId;

    protected Primitive()
    {
        try
        {
            _previousId = Id = checked (++_previousId) ?? 0;
        }
        catch (OverflowException ex)
        {
            throw new OverflowException("Cannot instantiate more than (int.MaxValue) unique primitives.", ex);
        }
    }
}

我认为我做错了 - 那么我如何正确测试它?

So basically, I have an abstract class which has a unique, incremental ID - Primitive. When a Primitive (or more precisely, an inheritor of Primitive) is instantiated, the ID is incremented - up to the point where the ID overflows - at which point, I add a message to the exception and rethrow.

OK, that all works fine... but I'm trying to test this functionality and I've never used mocking before. I just need to make enough Primitives for the ID to overflow and assert that it throws at the right time.

  • It is unreasonable to instantiate 2 billion objects to do this! However I don't see another way.
  • I don't know if I'm using mocking correctly? (I'm using Moq.)

Here's my test (xUnit):

[Fact(DisplayName = "Test Primitive count limit")]
public void TestPrimitiveCountLimit()
{
    Assert.Throws(typeof(OverflowException), delegate()
    {
        for (; ; )
        {
            var mock = new Mock<Primitive>();
        }
    });
}

and:

public abstract class Primitive
{
    internal int Id { get; private set; }
    private static int? _previousId;

    protected Primitive()
    {
        try
        {
            _previousId = Id = checked (++_previousId) ?? 0;
        }
        catch (OverflowException ex)
        {
            throw new OverflowException("Cannot instantiate more than (int.MaxValue) unique primitives.", ex);
        }
    }
}

I assume I'm doing it wrong - so how do I test this properly?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

我不吻晚风 2024-10-08 17:23:52

您不需要为此进行模拟。当两个类一起工作并且您想要用模拟(假)类替换一个类时,您可以使用模拟,这样您只需测试另一个类。您的示例中并非如此。

但是,有一种方法可以使用模拟,并且可以解决 20 亿个实例的问题。如果将 ID 生成与 Primitive 类分开并使用生成器,则您可以可以模拟生成器。示例:

我已更改 Primitive 以使用提供的生成器。在本例中,它被设置为静态变量,并且有更好的方法,但作为示例:

public abstract class Primitive
{
    internal static IPrimitiveIDGenerator Generator;

    protected Primitive()
    {
        Id = Generator.GetNext();
    }

    internal int Id { get; private set; }
}

public interface IPrimitiveIDGenerator
{
    int GetNext();
}

public class PrimitiveIDGenerator : IPrimitiveIDGenerator
{
    private int? _previousId;

    public int GetNext()
    {
        try
        {
            _previousId = checked(++_previousId) ?? 0;

            return _previousId.Value;
        }
        catch (OverflowException ex)
        {
            throw new OverflowException("Cannot instantiate more than (int.MaxValue) unique primitives.", ex);
        }
    }
}

然后,您的测试用例变为:

[Fact(DisplayName = "Test Primitive count limit")]
public void TestPrimitiveCountLimit()
{
    Assert.Throws(typeof(OverflowException), delegate()
    {
        var generator = new PrimitiveIDGenerator();

        for (; ; )
        {
            generator.GetNext();
        }
    });
}

这将运行得更快,现在您只需测试 ID 生成器是否工作。

现在,当您想要测试创建一个新原语实际上是否需要 ID 时,您可以尝试以下操作:

public void Does_primitive_ask_for_an_ID()
{
    var generator = new Mock<IPrimitiveIDGenerator>();

    // Set the expectations on the mock so that it checks that
    // GetNext is called. How depends on what mock framework you're using.

    Primitive.Generator = generator;

    new ChildOfPrimitive();
}

现在您已经分离了不同的关注点,可以单独测试它们。

You don't need mocking for this. You use mocking when two classes work together and you want to replace one class with a mock (fake) one so you only have to test the other one. This is not the case in your example.

There is however a way you could use mocks, and that fixes your issue with the 2bln instances. If you separate the ID generation from the Primitive class and use a generator, you can mock the generator. An example:

I've changed Primitive to use a provided generator. In this case it's set to a static variable, and there are better ways, but as an example:

public abstract class Primitive
{
    internal static IPrimitiveIDGenerator Generator;

    protected Primitive()
    {
        Id = Generator.GetNext();
    }

    internal int Id { get; private set; }
}

public interface IPrimitiveIDGenerator
{
    int GetNext();
}

public class PrimitiveIDGenerator : IPrimitiveIDGenerator
{
    private int? _previousId;

    public int GetNext()
    {
        try
        {
            _previousId = checked(++_previousId) ?? 0;

            return _previousId.Value;
        }
        catch (OverflowException ex)
        {
            throw new OverflowException("Cannot instantiate more than (int.MaxValue) unique primitives.", ex);
        }
    }
}

Then, your test case becomes:

[Fact(DisplayName = "Test Primitive count limit")]
public void TestPrimitiveCountLimit()
{
    Assert.Throws(typeof(OverflowException), delegate()
    {
        var generator = new PrimitiveIDGenerator();

        for (; ; )
        {
            generator.GetNext();
        }
    });
}

This will run a lot faster and now you're only testing whether the ID generator works.

Now, when you e.g. want to test that creating a new primitive actually asks for the ID, you could try the following:

public void Does_primitive_ask_for_an_ID()
{
    var generator = new Mock<IPrimitiveIDGenerator>();

    // Set the expectations on the mock so that it checks that
    // GetNext is called. How depends on what mock framework you're using.

    Primitive.Generator = generator;

    new ChildOfPrimitive();
}

Now you have separated the different concerns and can test them separately.

感性不性感 2024-10-08 17:23:52

模拟的目的是模拟外部资源。这不是你想要的,你想要测试你的对象,在这个场景中不需要模拟。如果您愿意,只需实例化 20 亿个对象即可,这并没有什么坏处,因为 GC 会丢弃旧实例(但可能需要一段时间才能完成)。

Id' 实际上添加了另一个构造函数,它接受身份计数器的起始值,这样您实际上可以从接近 int.MaxValue 开始,因此不需要实例化那么多对象。

另外,只需阅读源代码,我就可以知道您的对象将无法通过测试。 ;-)

The point of the mock is to simulate an external resource. It's not what you want, you want to test your object, no mock needed in this szenario. Just instantiate the 2 billion objects if you like to, it doesn't hurt since the GC will throw away the old instances (but may take a while to complete).

Id' actually add another constructor which accepts a strarting value for the identity counter, so that you can actually start close to int.MaxValue and therefore don't need to instatiate as many objects.

Also, just from readin the source I can tell that your object will fail the test. ;-)

温馨耳语 2024-10-08 17:23:52

这个问题有两个问题:

  1. 如何对无法实例化的抽象类进行单元测试。
  2. 如何有效地对需要创建和销毁 20 亿个实例的功能进行单元测试。

我认为解决方案非常简单,尽管您必须稍微重新考虑对象的结构。

对于第一个问题,解决方案很简单,只需向您的测试项目添加一个继承 Primitive 的假冒产品,但不添加任何功能。然后,您可以实例化您的伪类,并且您仍然可以测试 Primitive 的功能。

public class Fake : Primitive { }

// and in your test...
Assert.Throws(typeof(OverflowException), delegate() { var f = new Fake(int.MaxValue); });

对于第二个问题,我将添加一个构造函数,该构造函数采用 int 作为先前的 ID,并使用构造函数链接来在实际代码中“不需要它”。 (但是你如何才能知道之前的 id?你不能在测试设置中将其设置为 int.MaxValue-1 吗?)将其视为依赖注入,但是你不注入任何复杂的东西;您只是注入一个简单的int。可能是这样的:

public abstract class Primitive
{
internal int Id { get; private set; }
private static int? _previousId;

protected Primitive() : Primitive([some way you get your previous id now...])
protected Primitive(int previousId)
{
    _previousId = previousId;
    try
    {
        _previousId = Id = checked (++_previousId) ?? 0;
    }
    catch (OverflowException ex)
    {
        throw new OverflowException("Cannot instantiate more than (int.MaxValue) unique primitives.", ex);
    }
}

You have two problems baked into this question:

  1. How to unit test an abstract class, that you can't instantiate.
  2. How to efficiently unit test functionality that requires two billion instances to be created and destroyed.

I think the solutions are pretty simple, even though you'll have to re-think the structure of your object slightly.

For the first problem, the solution is as simple as adding a fake that inherits Primitive, but adds no functionality, to your test project. You can then instantiate your fake class instead, and you'll still be testing the functionality of Primitive.

public class Fake : Primitive { }

// and in your test...
Assert.Throws(typeof(OverflowException), delegate() { var f = new Fake(int.MaxValue); });

For the second problem, I'd add a constructor that takes an int for the previous ID, and use constructor chaining to "not need it" in your actual code. (But how to you get to know of the previous id otherwise? Can't you set that to int.MaxValue-1 in the setup of your test?) Think of it as dependecy injection, but you're not injecting anything complex; you're just injecting a simple int. It could be something along these lines:

public abstract class Primitive
{
internal int Id { get; private set; }
private static int? _previousId;

protected Primitive() : Primitive([some way you get your previous id now...])
protected Primitive(int previousId)
{
    _previousId = previousId;
    try
    {
        _previousId = Id = checked (++_previousId) ?? 0;
    }
    catch (OverflowException ex)
    {
        throw new OverflowException("Cannot instantiate more than (int.MaxValue) unique primitives.", ex);
    }
}
月隐月明月朦胧 2024-10-08 17:23:52

其他答案都已经说了。我只是想向您展示另一种选择,也许这对您来说有点有趣。

如果您将 Primitive 类的 _previousId 字段设置为 internal(当然,并包含相应的 InternalsVisibleTo 属性) ),那么使用 Typemock Isolator 工具,您的测试就可以像这样简单:

[Fact(DisplayName = "Test Primitive count limit"), Isolated]
public void TestPrimitiveCountLimit()
{
    Primitive._previousId = int.MaxValue;

    Assert.Throws<OverflowException>(() => 
        Isolate.Fake.Instance<Primitive>(Members.CallOriginal, ConstructorWillBe.Called));
}

当然,Typemock 附带了一些许可证成本,但如果您必须编写大量测试代码,它肯定会让生活变得更加轻松并节省大量时间 - 特别是在不容易测试甚至无法使用免费模拟框架进行测试的系统上。

托马斯

All has been said in the other answers. I just want to show you an alternative, maybe this is somehow interesting for you.

If you made the _previousId field of your Primitive class internal (and included the respective InternalsVisibleTo attribute, of course), then your test could be as simple as this with the Typemock Isolator tool:

[Fact(DisplayName = "Test Primitive count limit"), Isolated]
public void TestPrimitiveCountLimit()
{
    Primitive._previousId = int.MaxValue;

    Assert.Throws<OverflowException>(() => 
        Isolate.Fake.Instance<Primitive>(Members.CallOriginal, ConstructorWillBe.Called));
}

Sure, Typemock comes with some license costs, but it definitely makes life much easier and saves you a lot of time, if you have to write large amounts of test code - especially on systems which are not easily tested or are even impossible to test with a free mocking framework.

Thomas

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文