-->

signed

QiShunwang

“诚信为本、客户至上”

单元测试Unit Testing [22]

2021/6/3 13:23:15   来源:

mocks和测试的脆弱性

本章包括

  • 区分mocks和stubs
  • 定义可观察的行为和实施细节
  • 了解mocks和测试脆弱性之间的关系
  • 在不影响抵御重构的情况下使用mocks

  第4章介绍了一个参考框架,你可以用它来分析特定的测试和单元测试方法。在本章中,你将看到这个参考框架的作用;我们将用它来剖析mocks这个主题。
  在测试中使用mocks是一个有争议的话题。 有些人认为mocks是一个很好的工具,并在他们的大多数测试中使用。另一些人则声称,mocks会导致测试的脆弱性,并试图不使用它们。俗话说得好,真理就在这两者之间。在这一章中,我将表明,确实,mocks经常导致脆弱的测试–缺乏抵抗重构的指标的测试。 但仍有一些情况下,mocks是适用的,甚至是可取的。
  本章在很大程度上借鉴了第二章中关于单元测试的伦敦学派和古典学派的讨论。简而言之,这些流派之间的分歧源于他们对测试隔离问题的看法。伦敦学派主张将被测试的代码片断相互隔离,除了不可改变的依赖关系外,其他的都使用测试替身来执行这种隔离。
  古典学派主张隔离单元测试本身,以便它们可以并行运行。 这个流派只对测试之间共享的依赖关系使用测试替身。
  在模拟和测试的脆弱性之间有一种深刻的、几乎不可避免的联系。在接下来的几节中,我将逐步为你打下基础,让你明白为什么会有这种联系。你还将学习如何使用mocks,使其不影响测试对重构的抵抗力。

5.1 区分mock和stubs

  在第二章中,我简要地提到,mock是一种测试替身,它允许你检查被测系统(SUT)和它的合作者之间的交互。 还有一种类型的测试替身:stub。让我们仔细看看什么是mock,以及它与stub有什么不同。

5.1.1 测试替身的类型

  测试替身是一个总的术语,描述了测试中各种非生产就绪的、虚假的依赖关系。这个术语来自于电影中特技替身的概念。 测试替身的主要用途是促进测试;它们被传递给被测系统,而不是真正的依赖关系,后者可能很难设置或维护。
  根据Gerard Meszaros的说法,测试替身有五种变化:dummy、stub、spy、mock和fake。这样的种类看起来很吓人,但实际上,它们都可以归纳为两种类型:mock和stubs(图5.1)。

图5.1 所有测试替身的变化都可以分为两种类型:mocks和stubs。

 
  这两种类型的区别可归结为以下几点:

  • Mocks有助于模拟和检查即将发生的交互。这些交互是SUT对其依赖关系的调用,以改变它们的状态。
  • Stubs有助于模拟传入的交互。 这些交互是SUT对其依赖关系的调用,以获得输入数据(图5.2)。
图5.2 发送电子邮件是一个传出的交互:一个在SMTP服务器中产生副作用的交互行为。模拟这种交互的测试替身是一个Mock。从数据库中检索数据是一个传入的交互;它不会产生副作用。相应的测试替身是一个Stub。

 
  这五种变化之间的所有其他差异都是无关紧要的实现细节。例如,spies的作用与Mock相同。区别在于,spies是手动编写的,而mock是在mock框架的帮助下创建的。有时人们把spies称为手写的mock。
  另一方面,stub、dummy和fake之间的区别在于它们的智能程度。dummy是一个简单的、硬编码的值,比如一个空值或一个捏造的字符串。它被用来满足SUT的方法签名,不参与产生最终结果。stub是更复杂的。它是一个成熟的依赖关系,你可以配置它来为不同的情况返回不同的值。最后,在大多数情况下,fake和stub是一样的,区别在于创建它的理由:fake通常是为了替换一个不存在的依赖关系而实现的。
  请注意mocks和stubs之间的区别(除了传出与传入的交互)。 Mocks帮助模拟和检查SUT和它的依赖关系之间的交互,而stubs只帮助模拟这些交互。这是一个重要的区别。你很快就会看到原因。

5.1.2 Mock(工具)与mock(测试替身)的关系

  mock这个词是重载的,在不同的情况下可以有不同的含义。 我在第2章中提到,人们经常用这个词来指任何测试用的替身,而mock只是测试用替身的一个子集。但是mock这个词还有另外一个意思。你也可以把模拟库中的类称为mock。这些类可以帮助你创建实际的mock,但它们本身并不是mock。下面的列表显示了一个例子。

清单5.1 使用mock库中的Mock类来创建一个mock。

[Fact] 
public void Sending_a_greetings_email() {
	//使用一个模拟(工具)来创建一个模拟(测试替身)。
    var mock = new Mock < IEmailGateway > ();
    var sut = new Controller(mock.Object);
    sut.GreetUser("user@email.com");
    //检查从SUT到测试替身的调用情况
    mock.Verify(x => x.SendGreetingsEmail("user@email.com"), Times.Once);
}

  列表5.1中的测试使用了我选择的mock库(Moq)中的Mock类。这个类是一个工具,使你能够创建一个测试替身模拟。换句话说,Mock类(或Mock)是一个模拟(工具),而该类的实例,mock,是一个mock(测试替身)。重要的是不要把mock(工具)和mock(测试替身)混为一谈,因为你可以用mock(工具)来创建两种类型的测试替身:mock和stubs。
  下面列表中的测试也使用了Mock类,但该类的实例不是一个mock,而是一个stub。

清单5.2 使用Mock类来创建一个stub

[Fact] 
public void Creating_a_report() {
	//使用一个模拟(工具)来创建一个存根
    var stub = new Mock < IDatabase > ();
    //设置了一个预制的答案
    stub.Setup(x => x.GetNumberOfUsers()).Returns(10);
    var sut = new Controller(stub.Object);
    Report report = sut.CreateReport();
    Assert.Equal(10, report.NumberOfUsers);
}

  这个测试替身模拟了一个传入的交互–为SUT提供输入数据的调用。另一方面,在前面的例子中(清单5.1),对SendGreetingsEmail()的调用是一个传出交互。 它的唯一目的是产生一个副作用–发送一封电子邮件。