【问题标题】:What's the use of testing the interface rather than implementation?测试接口而不是实现有什么用?
【发布时间】:2015-07-08 00:16:43
【问题描述】:

我是 nSubstitute 的新手。我在很多文章中都看到过这样的代码示例。测试接口有什么用?以下测试何时会失败?根据IMathService的实际实现,下面的测试会失败吗?这里的想法是先在 TDD 中编写它,然后删除替代项并放入真正的实现?或者这个想法是测试一些真正的实现,但用 Nsubstitute 替换依赖项?如果是,则此示例没有显示。我很困惑。

public interface IMathService
{
    int Add(int a, int b);
}

[Test]
public void nSubstituteMockingInterface()
{
    var mathService = Substitute.For<IMathService>();
    mathService.Add(2,3).Returns(4);
    Assert.IsTrue(mathService.Add(2, 3) == 4);
}

假设我有以下实现,上面的测试会失败吗? Substitute.For() 行将返回 MyMathService 的实例?我相信,不是。如果是,如果我有多个 IMathService 实现会发生什么

class MyMathService:  IMathService
{
    public int Add(int a, int b)
    {
        throw new Exception("error");
    }
}

【问题讨论】:

    标签: c# unit-testing nsubstitute


    【解决方案1】:

    您完全正确,这些 sn-ps 不是您通常使用隔离框架的方式。这些只是展示如何使用 nSubsitute 库的用法示例。在您的示例中,测试将通过,因为它使用的是动态生成的对象,该对象遵循 IMathService 接口,但被编程为在使用参数 2 和 3 调用其 Add 方法时返回 4。真正的实现在任何地方都没有使用测试。

    nSubstitute 及其替代品可用于类及其协作者的交互测试。您可以使用这些类型的隔离框架来隔离测试类。通常,您创建被测系统 (SUT) 的具体实例,但为其合作者提供虚假的实现。然后,您可以对假合作者设置期望,或者断言对他们调用了某些方法来验证 SUT 是否与其合作者正确交互。您可以使用隔离框架为您自动执行这项重复性工作,而不是手动滚动您自己的假合作者。

    希望举个例子让事情更清楚。假设我正在研究 Foo 类,它正在使用我尚未实现的协作者 IBar。 IBar 将与真实数据库对话,因此我不想在 Foo 的独立单元测试中使用 IBar 的真实实现:

    public class Foo
    {
        private readonly IBar _bar;
    
        public Foo(IBar bar)
        {
            _bar = bar;
        }
    
        public void DoSomething()
        {
            _bar.DoSomethingElse();
        }
    }
    
    public interface IBar
    {
        void DoSomethingElse();
    }
    
    public class AnImplementationOfBar : IBar
    {
        public void DoSomethingElse()
        {
            throw new System.NotImplementedException();
        }
    }
    
    [Test]
    public void Foo_interacts_correctly_with_its_bar()
    {
        var fakeBar = Substitute.For<IBar>();
        var foo = new Foo(fakeBar);
    
        foo.DoSomething();
    
        fakeBar.Received().DoSomethingElse(); //This is the assertion of the test and verifies whether fakeBar.DoSomethingElse has been called.
    }
    
    public static void Main(string[] args)
    {
        //Production code would look like this:
        var foo = new Foo(new AnImplementationOfBar());
        //next line throws because AnImplementationOfBar.DoSomethingElse has not been implemented yet
        foo.DoSomething();
    }
    

    替代品大致可以用于两种场景:

    1. 检查是否发生了正确的交互(如我上面使用 Received() 方法的答案中的代码示例)。通常情况下,mock 一词用于此目的的赝品。
    2. 向 SUT 提供虚拟数据,以便生产代码执行特定的代码路径。通常,stub 一词用于此类赝品。 IMathService 测试是如何使用 Returns() 方法配置存根的典型示例。

    如果您熟悉单元测试的 Arrange-Act-Assert 定义:存根是测试的 Arrange 阶段的一部分,因为它们设置了您的 SUT 需要的一些虚拟数据。模拟是 Assert 部分的一部分,因为您使用它们来验证是否发生了特定交互。

    这种技术被大量用于某种测试驱动开发风格,称为外向内 TDD(也称为伦敦学校 TDD)。它允许您完全实现一个类并为其协作者指定契约,而无需实际实现协作者本身,而只是创建接口。它也可以用于其他场景,例如,当您进行单元测试并且不想使用真实的数据库、网络流量、第三方依赖项和/或使用起来非常困难或缓慢的类时测试。

    有关这些主题的更多信息,请查看 Martin Fowler 在Mocks Aren't Stubs 上的帖子。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-26
      • 2021-06-29
      • 1970-01-01
      • 2022-09-23
      • 2020-03-11
      相关资源
      最近更新 更多