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

时间:2010-11-07 13:24:04

标签: c# unit-testing .net-4.0 moq xunit

基本上,我有一个抽象类,它有一个唯一的增量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);
        }
    }
}

我认为我做错了 - 所以我该如何正确测试?

4 个答案:

答案 0 :(得分:5)

你不需要嘲笑。当两个类一起工作并且你想用mock(假)替换一个类时你使用mocking所以你只需要测试另一个类。在您的示例中不是这种情况。

但是有一种方法可以使用模拟,并修复2bln实例的问题。如果将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();
}

现在您已将不同的问题分开,并可以单独测试它们。

答案 1 :(得分:1)

模拟的重点是模拟外部资源。这不是你想要的,你想测试你的对象,在这个szenario中不需要模拟。如果您愿意,只需实例化20亿个对象,它就不会受到影响,因为GC会丢弃旧实例(但可能需要一段时间才能完成)。

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

另外,仅仅从阅读源代码我可以看出你的对象将无法通过测试。 ; - )

答案 2 :(得分:1)

这个问题涉及两个问题:

  1. 如何对抽象类进行单元测试,无法实例化。
  2. 如何有效地单元测试功能,需要创建和销毁20亿个实例。
  3. 我认为解决方案非常简单,即使您必须稍微重新考虑对象的结构。

    对于第一个问题,解决方案就像添加一个继承Primitive但未添加任何功能的假的简单到您的测试项目。然后,您可以实例化您的假类,并且您仍将测试Primitive的功能。

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

    对于第二个问题,我将添加一个构造函数,该构造函数对前一个ID采用int,并在实际代码中使用构造函数链接“不需要它”。 (但是你怎么知道以前的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);
        }
    }
    

答案 3 :(得分:1)

所有人都在其他答案中说过。我只是想向你展示另一种选择,也许这对你来说很有意思。

如果您创建了previousIdPrimitive的_ 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会带来一些许可证成本,但如果你必须编写大量的测试代码,它肯定会让生活变得更轻松并节省大量时间 - 特别是在那些不易测试甚至不可能的系统上使用免费的模拟框架进行测试。

托马斯