unit-testing-5.4-重新审视经典学派与伦敦学派

2026-05-21 ⏳2.5分钟(1.0千字)

5.4 The classical vs. London schools of unit testing, revisited

5.4 重新审视经典学派与伦敦学派

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

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

As a reminder from chapter 2, table 5.2 sums up the differences between the classical and London schools of unit testing.

作为第 2 章内容的回顾,表 5.2 总结了单元测试经典学派和伦敦学派之间的差异。

Table 5.2

In chapter 2, I mentioned that I prefer the classical school of unit testing over the London school. I hope now you can see why. The London school encourages the use of mocks for all but immutable dependencies and doesn’t differentiate between intra-system and inter-system communications. As a result, tests check communications between classes just as much as they check communications between your application and external systems.

第 2 章中我提到过,相比伦敦学派,我更偏好经典学派。希望现在你能看出原因。伦敦学派鼓励对除不可变依赖之外的所有依赖使用 mock,并且不区分系统内通信和系统间通信。结果是,测试会像检查应用与外部系统之间通信一样,检查类与类之间的通信。

This indiscriminate use of mocks is why following the London school often results in tests that couple to implementation details and thus lack resistance to refactoring. As you may remember from chapter 4, the metric of resistance to refactoring is mostly a binary choice: a test either has resistance to refactoring or it doesn’t. Compromising on this metric renders the test nearly worthless.

这种不加区分地使用 mock,正是遵循伦敦学派经常会导致测试耦合到实现细节、从而缺乏抵抗重构能力的原因。你可能还记得第 4 章讲过,抵抗重构这个指标基本上是二元选择:一个测试要么具备抵抗重构能力,要么不具备。在这个指标上妥协,几乎会让测试失去价值。

The classical school is much better at this issue because it advocates for substituting only dependencies that are shared between tests, which almost always translates into out-of-process dependencies such as an SMTP service, a message bus, and so on. But the classical school is not ideal in its treatment of inter-system communications, either. This school also encourages excessive use of mocks, albeit not as much as the London school.

经典学派在这个问题上要好得多,因为它主张只替换测试之间共享的依赖,而这几乎总是意味着进程外依赖,例如 SMTP 服务、消息总线等。不过,经典学派在处理系统间通信时也并不完美。这个学派也鼓励过度使用 mock,只是程度不如伦敦学派。

5.4.1 Not all out-of-process dependencies should be mocked out

5.4.1 并非所有进程外依赖都应该被 mock 掉

Before we discuss out-of-process dependencies and mocking, let me give you a quick refresher on types of dependencies:

在讨论进程外依赖和 mock 之前,我们先快速回顾一下依赖类型:

The classical school recommends avoiding shared dependencies because they provide the means for tests to interfere with each other’s execution context and thus prevent those tests from running in parallel. The ability for tests to run in parallel, sequentially, and in any order is called test isolation.

经典学派建议避免共享依赖,因为共享依赖会让测试之间有机会干扰彼此的执行上下文,从而阻止这些测试并行运行。测试能够并行、顺序以及以任意顺序运行的能力,称为测试隔离。

If a shared dependency is not out-of-process, then it’s easy to avoid reusing it in tests by providing a new instance of it on each test run. In cases where the shared dependency is out-of-process, testing becomes more complicated. You can’t instantiate a new database or provision a new message bus before each test execution; that would drastically slow down the test suite. The usual approach is to replace such dependencies with test doubles—mocks and stubs.

如果共享依赖不是进程外依赖,那么很容易避免在测试中复用它:每次测试运行时提供一个新实例即可。当共享依赖是进程外依赖时,测试就会变得更复杂。你不能在每次测试执行前都实例化一个新数据库或配置一个新消息总线;那会大幅拖慢测试套件。通常做法是用测试替身替换这些依赖,也就是 mock 和 stub。

Not all out-of-process dependencies should be mocked out, though. If an out-of-process dependency is only accessible through your application, then communications with such a dependency are not part of your system’s observable behavior. An out-of-process dependency that can’t be observed externally, in effect, acts as part of your application.

不过,并非所有进程外依赖都应该被 mock 掉。如果某个进程外依赖只能通过你的应用访问,那么与这个依赖的通信就不是系统可观察行为的一部分。一个无法从外部观察到的进程外依赖,实际上就像应用的一部分。

Figure 5.14

Remember, the requirement to always preserve the communication pattern between your application and external systems stems from the necessity to maintain backward compatibility. You have to maintain the way your application talks to external systems. That’s because you can’t change those external systems simultaneously with your application; they may follow a different deployment cycle, or you might simply not have control over them.

请记住,始终保持应用与外部系统之间通信模式不变的要求,源于维护向后兼容性的必要性。你必须维持应用与外部系统通信的方式。这是因为你不能总是让这些外部系统和你的应用同时变化;它们可能遵循不同的部署周期,或者你根本无法控制它们。

But when your application acts as a proxy to an external system, and no client can access it directly, the backward-compatibility requirement vanishes. Now you can deploy your application together with this external system, and it won’t affect the clients. The communication pattern with such a system becomes an implementation detail.

但当你的应用充当某个外部系统的代理,并且没有客户端能直接访问该外部系统时,向后兼容性的要求就消失了。此时你可以把应用和这个外部系统一起部署,而不会影响客户端。与这种系统的通信模式就变成了实现细节。

A good example here is an application database: a database that is used only by your application. No external system has access to this database. Therefore, you can modify the communication pattern between your system and the application database in any way you like, as long as it doesn’t break existing functionality. Because that database is completely hidden from the eyes of the clients, you can even replace it with an entirely different storage mechanism, and no one will notice.

一个很好的例子是应用数据库:只被你的应用使用的数据库。没有外部系统能访问这个数据库。因此,只要不破坏现有功能,你可以用任何方式修改系统与应用数据库之间的通信模式。因为这个数据库完全对客户端隐藏,所以你甚至可以把它替换成完全不同的存储机制,也没人会注意到。

The use of mocks for out-of-process dependencies that you have a full control over also leads to brittle tests. You don’t want your tests to turn red every time you split a table in the database or modify the type of one of the parameters in a stored procedure. The database and your application must be treated as one system.

对你完全控制的进程外依赖使用 mock,也会导致脆弱测试。你不会希望每次拆分数据库表,或修改某个存储过程参数类型时,测试都变红。数据库和你的应用必须被视为一个系统。

This obviously poses an issue. How would you test the work with such a dependency without compromising the feedback speed, the third attribute of a good unit test? You’ll see this subject covered in depth in the following two chapters.

这显然带来一个问题:如何在不牺牲反馈速度,也就是优秀单元测试第三个属性的前提下,测试与这类依赖的协作?接下来的两章会深入讨论这个主题。

5.4.2 Using mocks to verify behavior

5.4.2 使用 mock 验证行为

Mocks are often said to verify behavior. In the vast majority of cases, they don’t. The way each individual class interacts with neighboring classes in order to achieve some goal has nothing to do with observable behavior; it’s an implementation detail.

人们经常说 mock 用来验证行为。但在绝大多数情况下,它们并不是。每个单独类为了达成某个目标而与相邻类交互的方式,与可观察行为没有关系;它只是实现细节。

Verifying communications between classes is akin to trying to derive a person’s behavior by measuring the signals that neurons in the brain pass among each other. Such a level of detail is too granular. What matters is the behavior that can be traced back to the client goals. The client doesn’t care what neurons in your brain light up when they ask you to help. The only thing that matters is the help itself—provided by you in a reliable and professional fashion, of course.

验证类与类之间的通信,就像试图通过测量大脑神经元之间传递的信号来推导一个人的行为。这种细节层级太细了。真正重要的是能够追溯到客户端目标的行为。当客户端请求你帮忙时,他们并不关心你大脑中哪些神经元被激活。他们唯一关心的是帮助本身——当然,是由你以可靠且专业的方式提供的帮助。

Mocks have something to do with behavior only when they verify interactions that cross the application boundary and only when the side effects of those interactions are visible to the external world.

只有当 mock 验证的是跨越应用边界的交互,并且这些交互的副作用对外部世界可见时,mock 才与行为有关。