测试接口而不是测试实现有什么用?

本文关键字:测试 什么 接口 实现 | 更新日期: 2023-09-27 18:01:25

我是新手。我在许多文章中看到过这样的代码示例。测试接口有什么用?下面的测试什么时候会失败?基于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);
}

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

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

测试接口而不是测试实现有什么用?

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

nSubstitute及其替代品可用于类及其合作者的交互测试。您可以使用这些类型的隔离框架对类进行隔离测试。通常,您创建一个具体的被测系统(System Under Test, 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提供虚拟数据,以便生产代码执行特定的代码路径。通常,存根这个词用于这类伪造。IMathService测试是如何使用Returns()方法配置存根的典型示例。

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

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

有关这些主题的更多信息,请查看Martin Fowler的文章《mock Aren't stub》