unit-testing-5.1-区分mock和stub

2026-05-21 ⏳4.6分钟(1.8千字)

5.1 Differentiating mocks from stubs

5.1 区分 mock 和 stub

仅个人学习使用,支持正版。

书名:Unit Testing: Principles, Practices, and Patterns

This chapter draws heavily on the discussion about the London versus classical schools of unit testing from chapter 2. In short, the disagreement between the schools stems from their views on the test isolation issue. The London school advocates isolating pieces of code under test from each other and using test doubles for all but immutable dependencies to perform such isolation.

本章大量依赖第 2 章中关于伦敦学派与经典学派的讨论。简而言之,两个学派的分歧源于它们对测试隔离问题的看法。伦敦学派主张把被测代码片段彼此隔离,并使用测试替身替换除不可变依赖之外的所有依赖,以实现这种隔离。

The classical school stands for isolating unit tests themselves so that they can be run in parallel. This school uses test doubles only for dependencies that are shared between tests.

经典学派主张隔离单元测试本身,使它们能够并行运行。这个学派只会为测试之间共享的依赖使用测试替身。

There’s a deep and almost inevitable connection between mocks and test fragility. In the next several sections, I will gradually lay down the foundation for you to see why that connection exists. You will also learn how to use mocks so that they don’t compromise a test’s resistance to refactoring.

mock 与测试脆弱性之间存在一种深层且几乎不可避免的联系。接下来的几节会逐步铺垫,让你看清这种联系为什么存在。你也会学到如何使用 mock,而不损害测试的抵抗重构能力。

In chapter 2, I briefly mentioned that a mock is a test double that allows you to examine interactions between the system under test (SUT) and its collaborators. There’s another type of test double: a stub. Let’s take a closer look at what a mock is and how it is different from a stub.

第 2 章中我简要提到过,mock 是一种测试替身,它允许你检查被测系统(SUT)与其协作者之间的交互。还有另一种测试替身:stub。下面我们更仔细地看看 mock 是什么,以及它与 stub 有什么不同。

5.1.1 The types of test doubles

5.1.1 测试替身的类型

A test double is an overarching term that describes all kinds of non-production-ready, fake dependencies in tests. The term comes from the notion of a stunt double in a movie. The major use of test doubles is to facilitate testing; they are passed to the system under test instead of real dependencies, which could be hard to set up or maintain.

测试替身是一个总称,用来描述测试中各种非生产级的、伪造的依赖。这个术语来自电影中的替身演员概念。测试替身的主要用途是简化测试;它们会被传给被测系统,用来替代真实依赖,因为真实依赖可能很难设置或维护。

According to Gerard Meszaros, there are five variations of test doubles: dummy, stub, spy, mock, and fake. Such a variety can look intimidating, but in reality, they can all be grouped together into just two types: mocks and stubs.

根据 Gerard Meszaros 的分类,测试替身有五种变体:dummy、stub、spy、mock 和 fake。这样的种类看起来可能有点吓人,但实际上它们都可以归为两大类:mock 和 stub。

Figure 5.1

The difference between these two types boils down to the following:

这两类之间的差异可以归结为:

Figure 5.2

All other differences between the five variations are insignificant implementation details. For example, spies serve the same role as mocks. The distinction is that spies are written manually, whereas mocks are created with the help of a mocking framework. Sometimes people refer to spies as handwritten mocks.

这五种变体之间的其他差异,都是不重要的实现细节。例如,spy 和 mock 扮演相同角色。区别在于,spy 是手写的,而 mock 是借助 mocking 框架创建的。有时人们会把 spy 称为手写 mock。

On the other hand, the difference between a stub, a dummy, and a fake is in how intelligent they are. A dummy is a simple, hardcoded value such as a null value or a made-up string. It’s used to satisfy the SUT’s method signature and doesn’t participate in producing the final outcome. A stub is more sophisticated. It’s a fully fledged dependency that you configure to return different values for different scenarios. Finally, a fake is the same as a stub for most purposes. The difference is in the rationale for its creation: a fake is usually implemented to replace a dependency that doesn’t yet exist.

另一方面,stub、dummy 和 fake 的差异在于它们有多“智能”。dummy 是一个简单的硬编码值,比如 null 或一个随便构造的字符串。它用于满足 SUT 的方法签名,并不参与最终结果的生成。stub 更复杂一些。它是一个完整的依赖,你可以配置它在不同场景中返回不同值。最后,在大多数场景中,fake 和 stub 是一样的。区别在于创建它的原因:fake 通常用于替代一个尚不存在的依赖。

Notice the difference between mocks and stubs (aside from outcoming versus incoming interactions). Mocks help to emulate and examine interactions between the SUT and its dependencies, while stubs only help to emulate those interactions. This is an important distinction. You will see why shortly.

请注意 mock 和 stub 的差别(除了向外交互和向内交互之外)。mock 帮助模拟并检查 SUT 与其依赖之间的交互,而 stub 只帮助模拟这些交互。这是一个重要区别,你很快会看到原因。

5.1.2 Mock (the tool) vs. mock (the test double)

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

The term mock is overloaded and can mean different things in different circumstances. I mentioned in chapter 2 that people often use this term to mean any test double, whereas mocks are only a subset of test doubles. But there’s another meaning for the term mock. You can refer to the classes from mocking libraries as mocks, too. These classes help you create actual mocks, but they themselves are not mocks per se. The following listing shows an example.

mock 这个术语被重载了,在不同上下文中可能表示不同含义。第 2 章中我提到过,人们经常用这个词表示任何测试替身,但 mock 实际上只是测试替身的一个子集。不过 mock 还有另一个含义。你也可以把 mocking 库中的类称为 mock。这些类帮助你创建真正的 mock,但它们本身并不一定就是 mock。下面的代码清单展示了一个例子。

Listing 5.1 Using the Mock class from a mocking library to create a mock

[Fact]
public void Sending_a_greetings_email()
{
    var mock = new Mock<IEmailGateway>();
    var sut = new Controller(mock.Object);

    sut.GreetUser("user@email.com");

    mock.Verify(
        x => x.SendGreetingsEmail(
            "user@email.com"),
        Times.Once);
}

The test in listing 5.1 uses the Mock class from the mocking library of my choice (Moq). This class is a tool that enables you to create a test double—a mock. In other words, the class Mock (or Mock<IEmailGateway>) is a mock (the tool), while the instance of that class, mock, is a mock (the test double). It’s important not to conflate a mock (the tool) with a mock (the test double) because you can use a mock (the tool) to create both types of test doubles: mocks and stubs.

清单 5.1 中的测试使用了我选择的 mocking 库(Moq)中的 Mock 类。这个类是一个工具,让你能够创建测试替身——也就是 mock。换句话说,Mock 类(或 Mock<IEmailGateway>)是 mock(工具),而该类的实例 mock 是 mock(测试替身)。不要混淆 mock(工具)和 mock(测试替身),这一点很重要,因为你可以用 mock(工具)创建两种测试替身:mock 和 stub。

The test in the following listing also uses the Mock class, but the instance of that class is not a mock, it’s a stub.

下面代码清单中的测试也使用了 Mock 类,但这个类的实例不是 mock,而是 stub。

Listing 5.2 Using the Mock class to create a 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);
}

This test double emulates an incoming interaction—a call that provides the SUT with input data. On the other hand, in the previous example (listing 5.1), the call to SendGreetingsEmail() is an outcoming interaction. Its sole purpose is to incur a side effect—send an email.

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

5.1.3 Don’t assert interactions with stubs

5.1.3 不要对 stub 的交互做断言

As I mentioned in section 5.1.1, mocks help to emulate and examine outcoming interactions between the SUT and its dependencies, while stubs only help to emulate incoming interactions, not examine them. The difference between the two stems from the guideline of never asserting interactions with stubs. A call from the SUT to a stub is not part of the end result the SUT produces. Such a call is only a means to produce the end result: a stub provides input from which the SUT then generates the output.

正如 5.1.1 节中提到的,mock 帮助模拟并检查 SUT 与其依赖之间的向外交互,而 stub 只帮助模拟向内交互,并不检查它们。二者差异源于一条准则:永远不要对 stub 的交互做断言。从 SUT 到 stub 的调用并不是 SUT 产生的最终结果的一部分。这样的调用只是产生最终结果的手段:stub 提供输入,SUT 再根据输入生成输出。

NOTE Asserting interactions with stubs is a common anti-pattern that leads to fragile tests.

注意 对 stub 的交互做断言是一种常见反模式,会导致脆弱测试。

As you might remember from chapter 4, the only way to avoid false positives and thus improve resistance to refactoring in tests is to make those tests verify the end result (which, ideally, should be meaningful to a non-programmer), not implementation details. In listing 5.1, the check

你可能还记得第 4 章讲过,避免假阳性、提升测试抵抗重构能力的唯一方法,是让测试验证最终结果(理想情况下,这个结果应该对非程序员也有意义),而不是验证实现细节。在清单 5.1 中,下面这个检查:

mock.Verify(x => x.SendGreetingsEmail("user@email.com"))

corresponds to an actual outcome, and that outcome is meaningful to a domain expert: sending a greetings email is something business people would want the system to do. At the same time, the call to GetNumberOfUsers() in listing 5.2 is not an outcome at all. It’s an internal implementation detail regarding how the SUT gathers data necessary for the report creation. Therefore, asserting this call would lead to test fragility: it shouldn’t matter how the SUT generates the end result, as long as that result is correct. The following listing shows an example of such a brittle test.

对应的是一个真实结果,而且这个结果对领域专家有意义:发送问候邮件是业务人员希望系统完成的事情。与此同时,清单 5.2 中对 GetNumberOfUsers() 的调用根本不是结果。它是 SUT 为创建报表而收集必要数据的内部实现细节。因此,对这个调用做断言会导致测试脆弱:只要最终结果正确,SUT 如何生成这个结果并不重要。下面的代码清单展示了这样一个脆弱测试的例子。

Listing 5.3 Asserting an interaction with a 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);
    stub.Verify(
        x => x.GetNumberOfUsers(),
        Times.Once);
}

This practice of verifying things that aren’t part of the end result is also called overspecification. Most commonly, overspecification takes place when examining interactions. Checking for interactions with stubs is a flaw that’s quite easy to spot because tests shouldn’t check for any interactions with stubs. Mocks are a more complicated subject: not all uses of mocks lead to test fragility, but a lot of them do. You’ll see why later in this chapter.

这种验证不属于最终结果内容的做法,也叫过度指定。过度指定最常发生在检查交互时。检查与 stub 的交互是一个很容易发现的缺陷,因为测试不应该检查任何与 stub 的交互。mock 则更复杂:并不是所有 mock 用法都会导致测试脆弱,但很多确实会。本章后面会解释原因。

5.1.4 Using mocks and stubs together

5.1.4 同时使用 mock 和 stub

Sometimes you need to create a test double that exhibits the properties of both a mock and a stub. For example, here’s a test from chapter 2 that I used to illustrate the London style of unit testing.

有时你需要创建一个同时具备 mock 和 stub 属性的测试替身。例如,下面是第 2 章中用于说明伦敦学派单元测试风格的一个测试。

Listing 5.4 storeMock: both a mock and a stub

[Fact]
public void Purchase_fails_when_not_enough_inventory()
{
    var storeMock = new Mock<IStore>();
    storeMock
        .Setup(x => x.HasEnoughInventory(
            Product.Shampoo, 5))
        .Returns(false);

    var sut = new Customer();

    bool success = sut.Purchase(
        storeMock.Object, Product.Shampoo, 5);

    Assert.False(success);
    storeMock.Verify(
        x => x.RemoveInventory(Product.Shampoo, 5),
        Times.Never);
}

This test uses storeMock for two purposes: it returns a canned answer and verifies a method call made by the SUT. Notice, though, that these are two different methods: the test sets up the answer from HasEnoughInventory() but then verifies the call to RemoveInventory(). Thus, the rule of not asserting interactions with stubs is not violated here.

这个测试把 storeMock 用于两个目的:它返回一个预设答案,同时验证 SUT 发起的方法调用。不过请注意,这里涉及的是两个不同方法:测试为 HasEnoughInventory() 设置返回答案,但验证的是对 RemoveInventory() 的调用。因此,这里并没有违反“不要对 stub 的交互做断言”的规则。

When a test double is both a mock and a stub, it’s still called a mock, not a stub. That’s mostly the case because we need to pick one name, but also because being a mock is a more important fact than being a stub.

当一个测试替身既是 mock 又是 stub 时,它仍然被称为 mock,而不是 stub。这主要是因为我们需要选择一个名称,同时也是因为“它是 mock”这个事实比“它是 stub”更重要。

5.1.5 How mocks and stubs relate to commands and queries

5.1.5 mock 和 stub 与命令、查询的关系

The notions of mocks and stubs tie to the command query separation (CQS) principle. The CQS principle states that every method should be either a command or a query, but not both. Commands are methods that produce side effects and don’t return any value (void). Examples of side effects include mutating an object’s state, changing a file in the file system, and so on. Queries are the opposite of that—they are side-effect free and return a value.

mock 和 stub 的概念与命令查询分离(CQS)原则相关。CQS 原则认为,每个方法要么是命令,要么是查询,但不能两者都是。命令是产生副作用并且不返回任何值(void)的方法。副作用的例子包括修改对象状态、修改文件系统中的文件等。查询则相反——它们没有副作用,并且会返回一个值。

To follow this principle, be sure that if a method produces a side effect, that method’s return type is void. And if the method returns a value, it must stay side-effect free. In other words, asking a question should not change the answer. Code that maintains such a clear separation becomes easier to read. You can tell what a method does just by looking at its signature, without diving into its implementation details.

要遵循这个原则,需要确保如果一个方法会产生副作用,那么它的返回类型就是 void。如果一个方法返回值,那么它必须保持无副作用。换句话说,提出一个问题不应该改变答案。保持这种清晰分离的代码更容易阅读。你只需要看方法签名,就能知道它做什么,而不必深入实现细节。

Of course, it’s not always possible to follow the CQS principle. There are always methods for which it makes sense to both incur a side effect and return a value. A classical example is stack.Pop(). This method both removes a top element from the stack and returns it to the caller. Still, it’s a good idea to adhere to the CQS principle whenever you can.

当然,并不总是能遵循 CQS 原则。总有一些方法既产生副作用又返回值是合理的。经典例子是 stack.Pop()。这个方法既会从栈中移除顶部元素,也会把它返回给调用者。不过,只要可以,遵循 CQS 原则仍然是一个好主意。

Test doubles that substitute commands become mocks. Similarly, test doubles that substitute queries are stubs. Look at the two tests from listings 5.1 and 5.2 again:

替代命令的测试替身会成为 mock。类似地,替代查询的测试替身是 stub。再看清单 5.1 和 5.2 中两个测试的相关部分:

var mock = new Mock<IEmailGateway>();
mock.Verify(x => x.SendGreetingsEmail("user@email.com"));

var stub = new Mock<IDatabase>();
stub.Setup(x => x.GetNumberOfUsers()).Returns(10);

SendGreetingsEmail() is a command whose side effect is sending an email. The test double that substitutes this command is a mock. On the other hand, GetNumberOfUsers() is a query that returns a value and doesn’t mutate the database state. The corresponding test double is a stub.

SendGreetingsEmail() 是一个命令,它的副作用是发送电子邮件。替代这个命令的测试替身是 mock。另一方面,GetNumberOfUsers() 是一个查询,它返回一个值,并不会改变数据库状态。对应的测试替身是 stub。

Figure 5.3