【问题标题】:Confused about this unit test!对这个单元测试感到困惑!
【发布时间】:2010-11-07 13:24:03
【问题描述】:

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

好的,一切都很好......但我正在尝试测试这个功能,我以前从未使用过模拟。我只需要制作足够的 Primitives 以使 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);
        }
    }
}

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

【问题讨论】:

  • 你为什么要捕获 OverflowException 并用不同的消息抛出相同的异常?您应该简单地忽略该条件并让框架处理它。异常不应被 try/catch 块用作“返回码”。
  • 我认为当出现问题时它会更容易修复。也许评论会更合适。

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


【解决方案1】:

您不需要为此进行模拟。当两个类一起工作并且您想用一个模拟(假)替换一个类时,您使用模拟,因此您只需要测试另一个类.在您的示例中不是这种情况。

但是有一种方法可以使用模拟,它可以解决 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();
}

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

【讨论】:

  • 我想到了一个不同的想法并扩展了我的答案。这也解决了您的 2bln 个实例。关于测试抽象类:继承它用于单元测试。只需创建一个虚拟继承类。
  • 哇,答案很好。谢谢你,兄弟。我喜欢分离 ID 生成器的想法,然后我可以在其他地方重用它。我需要阅读我认为的模拟!
【解决方案2】:

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

Id' 实际上添加了另一个构造函数,该构造函数接受标识计数器的起始值,因此您实际上可以从接近 int.MaxValue 开始,因此不需要实例化尽可能多的对象。

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

【讨论】:

  • Primitive 是抽象的,所以无法直接创建。我应该只运行测试但实例化一个班级的孩子吗?
  • 只需创建一个仅供您的测试使用的PrimitiveTest: Primitive 类。
【解决方案3】:

这个问题有两个问题:

  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);
    }
}

【讨论】:

  • 我知道你来自哪里,它可以工作。我确实更喜欢 Pieter 的回答,但谢谢。
  • Pieter 的回答 is 实际上更好,因为他不仅对您的对象进行了一些必要的重组,而且还以一种很好的方式分离了关注点。
【解决方案4】:

其他答案中都已经说了。我只是想向您展示一个替代方案,也许这对您来说有点有趣。

如果您创建了Primitiveinternal 的_previousId 字段(当然还包括相应的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 会带来一些许可费用,但如果您必须编写大量测试代码(尤其是在不易测试甚至不可能测试的系统上),它肯定会让生活变得更轻松并为您节省大量时间使用免费的模拟框架进行测试。

托马斯

【讨论】:

    猜你喜欢
    • 2018-09-29
    • 2019-06-13
    • 2020-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-19
    • 2017-05-10
    • 1970-01-01
    相关资源
    最近更新 更多