wip-2.2
zero2.2 The classical and London schools of unit testing
2.2 单元测试的经典学派与伦敦学派
As you can see, the root of the differences between the London and classical schools is the isolation attribute. The London school views it as isolation of the system under test from its collaborators, whereas the classical school views it as isolation of unit tests themselves from each other.
如你所见,伦敦学派和经典学派之间差异的根源是隔离属性。伦敦学派把它看作被测系统与其协作者之间的隔离,而经典学派把它看作单元测试本身彼此之间的隔离。
This seemingly minor difference has led to a vast disagreement about how to approach unit testing, which, as you already know, produced the two schools of thought. Overall, the disagreement between the schools spans three major topics:
这个看似微小的差异,导致了关于如何实践单元测试的巨大分歧;正如你已经知道的,这种分歧产生了两个思想学派。总体来说,两个学派之间的分歧跨越三个主要主题:
- The isolation requirement
隔离要求。 - What constitutes a piece of code under test (a unit)
什么构成被测代码片段(一个单元)。 - Handling dependencies
如何处理依赖。
Table 2.1 sums it all up.
表 2.1 对这些内容做了总结。
2.2.1 How the classical and London schools handle dependencies
2.2.1 经典学派与伦敦学派如何处理依赖
Note that despite the ubiquitous use of test doubles, the London school still allows for using some dependencies in tests as-is. The litmus test here is whether a dependency is mutable. It’s fine not to substitute objects that don’t ever change—immutable objects.
注意,尽管伦敦学派大量使用测试替身,它仍然允许在测试中原样使用某些依赖。这里的试金石是依赖是否可变。对于永远不会变化的对象,也就是不可变对象,不替换它们是可以的。
And you saw in the earlier examples that, when I refactored the tests toward the London style, I didn’t replace the Product instances with mocks but rather used the real objects, as shown in the following code (repeated from listing 2.2 for your convenience):
在前面的例子中你也看到了,当我把测试重构为伦敦风格时,并没有用 mock 替换 Product 实例,而是使用真实对象。为了方便起见,下面重复清单 2.2 中的相关代码:
[Fact]
public void Purchase_fails_when_not_enough_inventory()
{
// Arrange
var storeMock = new Mock<IStore>();
storeMock
.Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
.Returns(false);
var customer = new Customer();
// Act
bool success = customer.Purchase(storeMock.Object, Product.Shampoo, 5);
// Assert
Assert.False(success);
storeMock.Verify(
x => x.RemoveInventory(Product.Shampoo, 5),
Times.Never);
}Of the two dependencies of Customer, only Store contains an internal state that can change over time. The Product instances are immutable (Product itself is a C# enum). Hence I substituted the Store instance only.
在 Customer 的两个依赖中,只有 Store 包含会随时间变化的内部状态。Product 实例是不可变的(Product 本身是 C# 枚举)。因此,我只替换了 Store 实例。
It makes sense, if you think about it. You wouldn’t use a test double for the 5 number in the previous test either, would you? That’s because it is also immutable—you can’t possibly modify this number. Note that I’m not talking about a variable containing the number, but rather the number itself. In the statement RemoveInventory(Product.Shampoo, 5), we don’t even use a variable; 5 is declared right away. The same is true for Product.Shampoo.
仔细想想,这很合理。你也不会在前面的测试中为数字 5 使用测试替身,对吧?这是因为它也是不可变的——你不可能修改这个数字。注意,我说的不是包含这个数字的变量,而是数字本身。在 RemoveInventory(Product.Shampoo, 5) 这条语句中,我们甚至没有使用变量;5 是直接声明出来的。Product.Shampoo 也是如此。
Such immutable objects are called value objects or values. Their main trait is that they have no individual identity; they are identified solely by their content. As a corollary, if two such objects have the same content, it doesn’t matter which of them you’re working with: these instances are interchangeable.
这种不可变对象被称为值对象或值。它们的主要特征是没有独立身份;它们完全由内容识别。因此,如果两个这样的对象内容相同,你使用哪一个都无所谓:这些实例可以互换。
For example, if you’ve got two 5 integers, you can use them in place of one another. The same is true for the products in our case: you can reuse a single Product.Shampoo instance or declare several of them—it won’t make any difference. These instances will have the same content and thus can be used interchangeably.
例如,如果你有两个整数 5,可以把它们互相替换使用。在我们的商品例子中也是如此:你可以复用一个 Product.Shampoo 实例,也可以声明多个这样的实例——这没有区别。这些实例会拥有相同内容,因此可以互换使用。
Note that the concept of a value object is language-agnostic and doesn’t require a particular programming language or framework. You can read more about value objects in my article “Entity vs. Value Object: The ultimate list of differences”.
注意,值对象这个概念与语言无关,并不要求特定编程语言或框架。你可以在我的文章“Entity vs. Value Object: The ultimate list of differences”中阅读更多关于值对象的内容。
Figure 2.4 shows the categorization of dependencies and how both schools of unit testing treat them. A dependency can be either shared or private. A private dependency, in turn, can be either mutable or immutable. In the latter case, it is called a value object.
图 2.4 展示了依赖的分类,以及两个单元测试学派如何处理这些依赖。依赖可以是共享的,也可以是私有的。私有依赖又可以是可变的或不可变的。后一种情况下,它被称为值对象。
For example, a database is a shared dependency—its internal state is shared across all automated tests (that don’t replace it with a test double). A Store instance is a private dependency that is mutable. And a Product instance (or an instance of a number 5, for that matter) is an example of a private dependency that is immutable—a value object. All shared dependencies are mutable, but for a mutable dependency to be shared, it has to be reused by tests.
例如,数据库是共享依赖——它的内部状态会在所有未用测试替身替换它的自动化测试之间共享。Store 实例是可变的私有依赖。而 Product 实例(或者数字 5 的实例)是不可变私有依赖,也就是值对象。所有共享依赖都是可变的,但一个可变依赖要成为共享依赖,必须被测试复用。
I’m repeating table 2.1 with the differences between the schools for your convenience.
为了方便起见,这里再次给出表 2.1 中两个学派之间的差异。
Collaborator vs. dependency
协作者与依赖
A collaborator is a dependency that is either shared or mutable. For example, a class providing access to the database is a collaborator since the database is a shared dependency. Store is a collaborator too, because its state can change over time. Product and number 5 are also dependencies, but they’re not collaborators. They’re values or value objects.
协作者是共享的或可变的依赖。例如,提供数据库访问的类是协作者,因为数据库是共享依赖。Store 也是协作者,因为它的状态会随时间变化。Product 和数字 5 也是依赖,但它们不是协作者。它们是值或值对象。
A typical class may work with dependencies of both types: collaborators and values. Look at this method call:
一个典型类可能会同时使用两类依赖:协作者和值。看看这个方法调用:
customer.Purchase(store, Product.Shampoo, 5)Here we have three dependencies. One of them (store) is a collaborator, and the other two (Product.Shampoo, 5) are not.
这里有三个依赖。其中一个(store)是协作者,另外两个(Product.Shampoo 和 5)不是。
Figure 2.5 shows the relation between shared and out-of-process dependencies.
图 2.5 展示了共享依赖与进程外依赖之间的关系。
For example, if there’s an API somewhere that returns a catalog of all products the organization sells, this isn’t a shared dependency as long as the API doesn’t expose the functionality to change the catalog. It’s true that such a dependency is volatile and sits outside the application’s boundary, but since the tests can’t affect the data it returns, it isn’t shared. This doesn’t mean you have to include such a dependency in the testing scope. In most cases, you still need to replace it with a test double to keep the test fast.
例如,如果某处有一个 API 会返回组织销售的所有商品目录,那么只要这个 API 不暴露修改目录的功能,它就不是共享依赖。这样的依赖确实是易变的,也位于应用边界之外,但由于测试无法影响它返回的数据,它就不是共享的。这并不意味着你必须把这样的依赖纳入测试范围。大多数情况下,你仍然需要用测试替身替换它,以保持测试快速。
But if the out-of-process dependency is quick enough and the connection to it is stable, you can make a good case for using it as-is in the tests.
但如果这个进程外依赖足够快,并且连接稳定,那么在测试中原样使用它也是说得通的。
Having that said, in this book, I use the terms shared dependency and out-of-process dependency interchangeably unless I explicitly state otherwise. In real-world projects, you rarely have a shared dependency that isn’t out-of-process. If a dependency is in-process, you can easily supply a separate instance of it to each test; there’s no need to share it between tests. Similarly, you normally don’t encounter an out-of-process dependency that doesn’t provide a means for tests to communicate with each other.
话虽如此,在本书中,除非我明确说明,否则我会把共享依赖和进程外依赖这两个术语互换使用。在真实项目中,你很少会遇到不是进程外的共享依赖。如果一个依赖位于进程内,你可以很容易地为每个测试提供一个单独实例;没有必要在测试之间共享它。类似地,你通常也不会遇到一个不会为测试之间相互通信提供手段的进程外依赖。