【问题标题】:C# Unit Test - To mock, stub or use explicit implementationC# 单元测试 - 模拟、存根或使用显式实现
【发布时间】:2013-04-17 10:40:30
【问题描述】:

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

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

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

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

您为什么建议我选择一个而不是另一个来验证该方法的行为是否符合预期? IE。 .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;
    }

干杯

【问题讨论】:

  • 我说让你的测试尽可能简单。恕我直言,仅当测试足够复杂以至于需要它时才进行模拟。如果您使用的是 VS2012,请尝试利用 fakes。在您的情况下,我认为您的第二种方法没有任何问题(我没有尝试过您的代码,但看起来不错)
  • @GabeThorns:但是如果测试“足够复杂”,则测试(或被测类)可能已经存在问题:-)
  • 我同意@Steven 测试不应该真的很“复杂”,因为这往往表明代码有异味

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


【解决方案1】:

这两种测试方法在我看来都很奇怪。使用第一种方法,您似乎没有测试任何东西(或者我可能误解了 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 个测试。 Disposed 在调用 dispose 之前应该为 false 不是该测试运行的先决条件;这是系统工作的要求。换句话说,您需要明确这一点并需要第二次测试。

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

【讨论】:

  • NSubstitute 替代方案(诚然应该分成两个测试)只是测试 .Dispose() 方法被调用一次,并且 Disposed 成员已设置为 true。我不觉得这特别有用,因为如果我自己明确设置 Disposed 当然等于 true?
  • 我不明白你的评论。当这是您需要测试的结果时,您为什么要将Disposed 设置为true?您要测试的是“未调用 Dispose() 时 Disposed 为假”和“调用 Dispose 时 Disposed 为真”。
  • 您的 NOT 和 ALWAYS 断言只是风格。您还断言 DI 容器不仅仅是一种请求依赖项的方式,当您想使用 DI 容器时,不需要使用 DI 框架。
  • Steven,这正是我的观点,为什么 NSubstitute 的这个功能会有任何用处。我想理想地使用接口进行测试,你会建议我使用工厂来创建假货(或类似 FakeItEasy)吗?
  • @JimmyHeaddon:也许我误解了你的问题,但总的来说,你应该伪造被测类(SUT);你使用真实的东西。你伪造它的依赖。对于创建虚假依赖项,您可以使用 NSubstitute。如果您伪造 SUT 本身,则根本没有什么可测试的。如果您发现在伪造它的依赖项时仍然难以测试 SUT,那么您的设计可能有问题。例如,您可能违反了 SOLID 原则,或者您没有实践 TDD。
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 2011-09-15
    • 2015-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-11
    • 2014-03-29
    • 1970-01-01
    相关资源
    最近更新 更多