C#单元测试 - 模拟,存根或使用显式实现

时间:2013-04-17 10:40:30

标签: c# unit-testing mocking simple-injector nsubstitute

以前已经讨论了很多次,但下面例子中的优点并不明显,所以请耐心等待。

我正在尝试决定是否在我的单元测试中使用模拟实现,并且考虑到以下两个示例尚未确定,第一个使用NSubstitute进行模拟,第二个使用SimpleInjector(Bootstrapper对象)解析实现。

基本上两者都在测试相同的东西,当调用.Dispose()方法时,Disposed成员被设置为true(参见本文底部的方法实现)。

在我看来,第二种方法对于回归测试更有意义,因为模拟代理在第一个示例中显式地将Disposed成员设置为true,而它是由注入的实现中的实际.Dispose()方法设置的。

为什么你建议我选择一个来验证方法是否符合预期?即调用.Dispose()方法,并通过此方法正确设置Disposed成员。

    [Test]
    public void Mock_socket_base_dispose_call_is_received()
    {
        var socketBase = Substitute.For<ISocketBase>();
        socketBase.Disposed.Should().BeFalse("this is the default disposed state.");

        socketBase.Dispose();
        socketBase.Received(1).Dispose();

        socketBase.Disposed.Returns(true);
        socketBase.Disposed.Should().BeTrue("the ISafeDisposable interface requires this.");
    }

    [Test]
    public void Socket_base_is_marked_as_disposed()
    {
        var socketBase = Bootstrapper.GetInstance<ISocketBase>();
        socketBase.Disposed.Should().BeFalse("this is the default disposed state.");
        socketBase.Dispose();
        socketBase.Disposed.Should().BeTrue("the ISafeDisposable interface requires this.");
    }

作为参考,.Dispose()方法就是这样:

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Releases unmanaged and - optionally - managed resources.
    /// </summary>
    /// <param name="disposeAndFinalize"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
    protected void Dispose(bool disposeAndFinalize)
    {
        if (Disposed)
        {
            return;
        }

        if (disposeAndFinalize)
        {
            DisposeManagedResources();
        }

        DisposeUnmanagedResources();

        Disposed = true;
    }

干杯

2 个答案:

答案 0 :(得分:3)

这两种测试方法对我来说都很奇怪。使用第一种方法,您似乎没有测试任何东西(或者我可能误解了NSubstitute的功能),因为您只是模拟ISocketBase接口(没有要测试的行为)并开始测试该模拟对象真正的实施。

第二种方法也很糟糕,因为你应该 NOT 在单元测试中使用任何DI容器。这只会使事情变得更复杂,因为:

  1. 您现在使用所有测试都使用的共享状态,这使得所有测试都相互依赖(测试应该独立运行)。
  2. 容器引导逻辑将变得非常复杂,因为您希望为不同的测试插入不同的模拟,并且在测试之间不会共享任何对象。
  3. 您的测试对框架或外观产生了额外的依赖性,而这种框架或外观无论如何都不存在。从这个意义上说,你只是让你的测试更复杂。它可能只是稍微复杂一些,但它仍然是一个额外的复杂功能。
  4. 相反,您应该做的是始终在单元测试(或测试工厂方法)本身内创建测试中的类(SUT)。您可能仍希望使用模拟框架创建SUT依赖项,但这是可选的。所以,IMO测试看起来应该是这样的:

    [Test]
    public void A_nondisposed_Socket_base_should_not_be_marked_dispose()
    {
        // Arrange
        Socket socket = CreateValidSocket();
    
        // Assert
        socketBase.Disposed.Should().BeFalse(
            "A non-disposed socket should not be flagged.");
    }
    
    [Test]
    public void Socket_base_is_marked_as_disposed_after_calling_dispose()
    {
        // Arrange
        Socket socket = CreateValidSocket();
    
        // Act
        socketBase.Dispose();
    
        // Assert
        socketBase.Disposed.Should().BeTrue(
            "Should be flagged as Disposed.");
    }
    
    private static Socket CreateValidSocket()
    {
        return new Socket(
            new FakeDependency1(), new FakeDependency2());
    }
    

    请注意,我将您的单个测试分成2个测试。调用dispose之前Disposed应为false,这不是运行该测试的前提条件;这是系统工作的要求。换句话说,你需要明确这一点,需要进行第二次测试。

    另请注意使用在多个测试中重复使用的CreateValidSocket工厂方法。当其他测试检查需要更多特定伪造或模拟对象的类的其他部分时,此方法可能有多个重载(或可选参数)。

答案 1 :(得分:1)

你太在意了。此测试是测试天气与否,给定的实现正确处理,因此您的测试应该反映出来。请参阅下面的伪代码。非脆性测试的技巧是仅测试满足测试所需的绝对最小值。

 public class When_disposed_is_called()
 {
    public void The_object_should_be_disposed()
    {
       var disposableObjects = someContainer.GetAll<IDisposable>();
       disposableObjects.ForEach(obj => obj.Dispose());
       Assert.False(disposableObject.Any(obj => obj.IsDisposed == false));
    }
 }

正如您所看到的,我在一个依赖容器中填充了我所关注的实现IDisposable的所有对象。我可能不得不嘲笑他们或做其他事情,但这不是测试的关注点。最终,它只关注验证当处理某些东西时,它实际上应该被处理掉。