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

之前已经对此进行了多次讨论,但是以下示例中的优点并不明显,因此请耐心等待.

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

本质上,两者都在测试同一件事,即当调用.Dispose()方法时,将Disposed成员设置为true(请参阅本文底部的method的实现).

在我看来,第二种方法对于回归测试更有意义,因为模拟代理在第一个示例中将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;
    }

干杯

解决方法:

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

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

>现在,您使用所有测试都使用的共享状态,这会使所有测试相互依赖(测试应独立运行).
>容器引导逻辑将变得非常复杂,因为您想为不同的测试插入不同的模拟,并且再次,在测试之间不共享任何对象.
>您的测试对根本不存在的框架或外观有额外的依赖.从这个意义上讲,您只是在使测试变得更加复杂.它可能会稍微复杂一点,但是仍然是一个额外的复杂性.

相反,您应该做的是始终在单元测试(或测试工厂方法)本身内部创建被测类(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之前Dispose应该为false并不是该测试运行的前提;这是系统正常运行的要求.换句话说,您需要对此进行明确说明,并需要进行第二次测试.

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

上一篇:NSubstitute可以模拟MethodInfo的返回吗?


下一篇:如何在C#单元测试中将测试Cookie添加到请求中