wip-3.3

2026-06-05 ⏳2.3分钟(0.9千字)

3.3 Reusing test fixtures between tests

3.3 在测试之间复用测试夹具

It’s important to know how and when to reuse code between tests. Reusing code between arrange sections is a good way to shorten and simplify your tests, and this section shows how to do that properly.

了解如何以及何时在测试之间复用代码很重要。在准备阶段之间复用代码,是缩短和简化测试的好方法。本节会展示如何正确做到这一点。

I mentioned earlier that often, fixture arrangements take up too much space. It makes sense to extract these arrangements into separate methods or classes that you then reuse between tests. There are two ways you can perform such reuse, but only one of them is beneficial; the other leads to increased maintenance costs.

我之前提到过,夹具准备经常占据太多空间。把这些准备逻辑提取到单独方法或类中,然后在测试之间复用,是合理的。你可以用两种方式执行这种复用,但只有其中一种是有益的;另一种会增加维护成本。

Test fixture

测试夹具

The term test fixture has two common meanings:

测试夹具这个术语有两个常见含义:

  1. A test fixture is an object the test runs against. This object can be a regular dependency—an argument that is passed to the SUT. It can also be data in the database or a file on the hard disk. Such an object needs to remain in a known, fixed state before each test run, so it produces the same result. Hence the word fixture.
    测试夹具是测试运行所针对的对象。这个对象可以是普通依赖,也就是传给 SUT 的参数;也可以是数据库中的数据,或硬盘上的文件。这样的对象需要在每次测试运行前保持在已知、固定的状态,以便产生相同结果。这就是 fixture 这个词的含义。
  2. The other definition comes from the NUnit testing framework. In NUnit, TestFixture is an attribute that marks a class containing tests.
    另一个定义来自 NUnit 测试框架。在 NUnit 中,TestFixture 是一个用于标记包含测试的类的特性。

I use the first definition throughout this book.

本书通篇使用第一个定义。

The first—incorrect—way to reuse test fixtures is to initialize them in the test’s constructor (or the method marked with a [SetUp] attribute if you are using NUnit), as shown next.

第一种也是不正确的复用测试夹具方式,是在测试构造函数中初始化它们(如果使用 NUnit,则是在标记为 [SetUp] 的方法中),如下所示。

Listing 3.7

The two tests in listing 3.7 have common configuration logic. In fact, their arrange sections are the same and thus can be fully extracted into CustomerTests’s constructor—which is precisely what I did here. The tests themselves no longer contain arrangements.

清单 3.7 中的两个测试拥有共同配置逻辑。事实上,它们的准备阶段是相同的,因此可以完全提取到 CustomerTests 的构造函数中——这正是我在这里做的事情。测试本身不再包含准备逻辑。

With this approach, you can significantly reduce the amount of test code—you can get rid of most or even all test fixture configurations in tests. But this technique has two significant drawbacks:

通过这种方式,你可以显著减少测试代码量——你可以去掉测试中大多数甚至全部测试夹具配置。但这种技术有两个显著缺点:

Let’s discuss these drawbacks in more detail.

下面更详细地讨论这些缺点。

3.3.1 High coupling between tests is an anti-pattern

3.3.1 测试之间高度耦合是一种反模式

In the new version, shown in listing 3.7, all tests are coupled to each other: a modification of one test’s arrangement logic will affect all tests in the class. For example, changing this line:

在清单 3.7 展示的新版本中,所有测试都彼此耦合:修改一个测试的准备逻辑会影响类中的所有测试。例如,把这一行:

_store.AddInventory(Product.Shampoo, 10);

to this:

改成这一行:

_store.AddInventory(Product.Shampoo, 15);

would invalidate the assumption the tests make about the store’s initial state and therefore would lead to unnecessary test failures.

就会使测试对商店初始状态的假设失效,从而导致不必要的测试失败。

That’s a violation of an important guideline: a modification of one test should not affect other tests. This guideline is similar to what we discussed in chapter 2—that tests should run in isolation from each other. It’s not the same, though. Here, we are talking about independent modification of tests, not independent execution. Both are important attributes of a well-designed test.

这违反了一条重要指南:修改一个测试不应影响其他测试。这条指南与第 2 章讨论的测试应该彼此隔离运行类似。不过它们并不相同。这里讨论的是测试的独立修改,而不是独立执行。二者都是设计良好测试的重要属性。

To follow this guideline, you need to avoid introducing shared state in test classes. These two private fields are examples of such a shared state:

要遵循这条指南,你需要避免在测试类中引入共享状态。下面两个私有字段就是这种共享状态的例子:

private readonly Store _store;
private readonly Customer _sut;

3.3.2 The use of constructors in tests diminishes test readability

3.3.2 在测试中使用构造函数会降低测试可读性

The other drawback to extracting the arrangement code into the constructor is diminished test readability. You no longer see the full picture just by looking at the test itself. You have to examine different places in the class to understand what the test method does.

把准备代码提取到构造函数中的另一个缺点,是测试可读性降低。你不再能仅通过查看测试本身看到完整画面。你必须查看类中的不同位置,才能理解测试方法做了什么。

Even if there’s not much arrangement logic—say, only instantiation of the fixtures—you are still better off moving it directly to the test method. Otherwise, you’ll wonder if it’s really just instantiation or something else being configured there, too. A self-contained test doesn’t leave you with such uncertainties.

即使准备逻辑不多,比如只是实例化夹具,把它直接移到测试方法中仍然更好。否则,你会疑惑那里是否真的只有实例化,还是还配置了其他东西。自包含的测试不会留下这种不确定性。

3.3.3 A better way to reuse test fixtures

3.3.3 更好的测试夹具复用方式

The use of the constructor is not the best approach when it comes to reusing test fixtures. The second way—the beneficial one—is to introduce private factory methods in the test class, as shown in the following listing.

当涉及复用测试夹具时,使用构造函数不是最佳方法。第二种方式,也就是有益的方式,是在测试类中引入私有工厂方法,如下面清单所示。

Listing 3.8

By extracting the common initialization code into private factory methods, you can also shorten the test code, but at the same time keep the full context of what’s going on in the tests. Moreover, the private methods don’t couple tests to each other as long as you make them generic enough. That is, allow the tests to specify how they want the fixtures to be created.

把公共初始化代码提取到私有工厂方法中,同样可以缩短测试代码,同时保留测试中发生事情的完整上下文。此外,只要你让这些私有方法足够通用,它们就不会把测试彼此耦合。也就是说,要允许测试指定它们希望如何创建夹具。

Look at this line, for example:

例如,看看这一行:

Store store = CreateStoreWithInventory(Product.Shampoo, 10);

The test explicitly states that it wants the factory method to add 10 units of shampoo to the store. This is both highly readable and reusable. It’s readable because you don’t need to examine the internals of the factory method to understand the attributes of the created store. It’s reusable because you can use this method in other tests, too.

测试明确说明它希望工厂方法向商店添加 10 件洗发水。这既高度可读,又可复用。它可读,是因为你不需要查看工厂方法内部,就能理解创建出来的商店具有什么属性。它可复用,是因为你也可以在其他测试中使用这个方法。

Note that in this particular example, there’s no need to introduce factory methods, as the arrangement logic is quite simple. View it merely as a demonstration.

注意,在这个特定例子中,并不需要引入工厂方法,因为准备逻辑相当简单。这里仅把它看作演示。

There’s one exception to this rule of reusing test fixtures. You can instantiate a fixture in the constructor if it’s used by all or almost all tests. This is often the case for integration tests that work with a database. All such tests require a database connection, which you can initialize once and then reuse everywhere. But even then, it would make more sense to introduce a base class and initialize the database connection in that class’s constructor, not in individual test classes. See the following listing for an example of common initialization code in a base class.

复用测试夹具这条规则有一个例外。如果某个夹具被所有或几乎所有测试使用,你可以在构造函数中实例化它。这在使用数据库的集成测试中经常出现。所有这类测试都需要数据库连接,你可以初始化一次,然后到处复用。但即便如此,引入一个基类,并在该基类构造函数中初始化数据库连接,也比在每个单独测试类中这么做更合理。下面清单展示了基类中的公共初始化代码示例。

Listing 3.9

Notice how CustomerTests remains constructor-less. It gets access to the _database instance by inheriting from the IntegrationTests base class.

注意,CustomerTests 仍然没有构造函数。它通过继承 IntegrationTests 基类来访问 _database 实例。