3

这个之前已经讨论过很多次了,但是下面例子中的优点并不明显,所以请多多包涵。

我正在尝试决定是否在我的单元测试中使用模拟实现,并且鉴于以下两个示例,我尚未决定,第一个使用 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;
    }

干杯

4

2 回答 2

3

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

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

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

相反,您应该始终在单元测试(或测试工厂方法)本身内创建被测类 (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在多个测试中重用的工厂方法的使用。当其他测试检查需要更具体的假对象或模拟对象的类的其他部分时,您可能对此方法有多个重载(或可选参数)。

于 2013-04-17T11:45:15.770 回答
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。我可能不得不嘲笑他们或做其他事情,但这不是测试的关注点。最终,它只关心验证当某物被处置时,它实际上应该被处置。

于 2013-04-17T11:31:44.317 回答