c#单元测试——模拟、存根或使用显式实现

本文关键字:实现 存根 单元测试 模拟 | 更新日期: 2023-09-27 18:09:51

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

我正试图决定是否在我的单元测试中使用模拟实现,并且给出以下两个示例,第一个使用NSubstitute进行模拟,第二个使用SimpleInjector (Bootstrapper对象)解决实现。

本质上两者都在测试同一件事,即当. dispose()方法被调用时,dispose成员被设置为true(参见本文底部的方法实现)。

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

为什么你会建议我选择一个而不是另一个来验证方法的行为是否如预期的那样?也就是说,调用了。dispose()方法,并且该方法正确地设置了dispose成员。

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

欢呼

c#单元测试——模拟、存根或使用显式实现

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

第二个方法也是不好的,因为你应该在单元测试中使用任何DI容器。这只会使事情变得更加复杂,因为:

  1. 您现在使用所有测试使用的共享状态,这使得所有测试相互依赖(测试应该单独运行)。
  2. 容器引导逻辑将变得非常复杂,因为您希望为不同的测试插入不同的mock,并且在测试之间不共享对象。
  3. 您的测试获得了对框架或facade的额外依赖,而这些框架或facade本来就不存在。从这个意义上说,您只是使您的测试更加复杂。它可能只是稍微复杂一点,但它仍然是一个额外的复杂性。
相反,您应该做的是总是在单元测试(或测试工厂方法)本身中创建被测类(SUT)。您可能仍然希望使用mock框架创建SUTs依赖项,但这是可选的。所以,在我看来,测试应该是这样的:
[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工厂方法的使用。当其他测试检查类中需要更具体的伪或模拟对象的其他部分时,此方法可能会有多个重载(或可选参数)。

你关心得太多了。这个测试是测试给定的实现是否正确处理,因此您的测试应该反映这一点。请参阅下面的伪代码。非脆性测试的诀窍是只测试满足测试所需的绝对最小值。

 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的所有对象填充了一些依赖容器。我可能不得不嘲笑他们或做其他事情,但这不是考试的重点。最终,它只涉及验证当某物被处置时,它实际上应该被处置。