wip-3.5

2026-06-05 ⏳2.1分钟(0.8千字)

3.5 Refactoring to parameterized tests

3.5 重构为参数化测试

One test usually is not enough to fully describe a unit of behavior. Such a unit normally consists of multiple components, each of which should be captured with its own test. If the behavior is complex enough, the number of tests describing it can grow dramatically and may become unmanageable. Luckily, most unit testing frameworks provide functionality that allows you to group similar tests using parameterized tests.

一个测试通常不足以完整描述一个行为单元。这样的单元通常由多个组成部分构成,每个组成部分都应该由自己的测试捕获。如果行为足够复杂,描述它的测试数量可能会急剧增长,并变得难以管理。幸运的是,大多数单元测试框架都提供了使用参数化测试分组相似测试的功能。

Figure 3.2

In this section, I’ll first show each such behavior component described by a separate test and then demonstrate how these tests can be grouped together.

本节中,我会先展示如何用单独测试描述每个行为组成部分,然后演示如何把这些测试组合到一起。

Let’s say that our delivery functionality works in such a way that the soonest allowed delivery date is two days from now. Clearly, the one test we have isn’t enough. In addition to the test that checks for a past delivery date, we’ll also need tests that check for today’s date, tomorrow’s date, and the date after that.

假设我们的配送功能规则是,最早允许的配送日期是两天后。显然,我们已有的一个测试是不够的。除了检查过去配送日期的测试之外,还需要检查今天、明天和后天日期的测试。

The existing test is called Delivery_with_a_past_date_is_invalid. We could add three more:

现有测试名为 Delivery_with_a_past_date_is_invalid。我们可以再添加三个测试:

public void Delivery_for_today_is_invalid()
public void Delivery_for_tomorrow_is_invalid()
public void The_soonest_delivery_date_is_two_days_from_now()

But that would result in four test methods, with the only difference between them being the delivery date.

但这样会产生四个测试方法,而它们之间唯一的区别只是配送日期。

A better approach is to group these tests into one in order to reduce the amount of test code. xUnit (like most other test frameworks) has a feature called parameterized tests that allows you to do exactly that. The next listing shows how such grouping looks. Each InlineData attribute represents a separate fact about the system; it’s a test case in its own right.

更好的方法是把这些测试合并为一个,以减少测试代码量。xUnit(和大多数其他测试框架一样)提供了名为参数化测试的功能,正好可以做到这一点。下一个清单展示了这种分组方式。每个 InlineData 特性都表示关于系统的一个独立事实;它本身就是一个测试用例。

Listing 3.11

TIP Notice the use of the [Theory] attribute instead of [Fact]. A theory is a bunch of facts about the behavior.

提示 注意这里使用 [Theory] 特性,而不是 [Fact]。一个 theory 是关于行为的一组事实。

Each fact is now represented by an [InlineData] line rather than a separate test. I also renamed the test method something more generic: it no longer mentions what constitutes a valid or invalid date.

现在,每个事实都由一行 [InlineData] 表示,而不是由一个单独测试表示。我还把测试方法重命名为更通用的名称:它不再提到什么构成有效或无效日期。

Using parameterized tests, you can significantly reduce the amount of test code, but this benefit comes at a cost. It’s now hard to figure out what facts the test method represents. And the more parameters there are, the harder it becomes. As a compromise, you can extract the positive test case into its own test and benefit from the descriptive naming where it matters the most—in determining what differentiates valid and invalid delivery dates, as shown in the following listing.

使用参数化测试可以显著减少测试代码量,但这种好处是有代价的。现在很难弄清楚这个测试方法表示了哪些事实。而参数越多,这就越困难。作为折中,你可以把正向测试用例提取成自己的测试,并在最重要的地方享受描述性命名带来的好处——也就是确定有效配送日期和无效配送日期的区别,如下面清单所示。

Listing 3.12

This approach also simplifies the negative test cases, since you can remove the expected Boolean parameter from the test method. And, of course, you can transform the positive test method into a parameterized test as well, to test multiple dates.

这种方法也简化了负向测试用例,因为你可以从测试方法中移除预期布尔参数。当然,你也可以把正向测试方法转换为参数化测试,以测试多个日期。

As you can see, there’s a trade-off between the amount of test code and the readability of that code. As a rule of thumb, keep both positive and negative test cases together in a single method only when it’s self-evident from the input parameters which case stands for what. Otherwise, extract the positive test cases. And if the behavior is too complicated, don’t use the parameterized tests at all. Represent each negative and positive test case with its own test method.

如你所见,测试代码量和代码可读性之间存在取舍。经验法则是:只有当从输入参数中一眼就能看出哪个用例代表什么时,才把正向和负向测试用例放在同一个方法中。否则,提取正向测试用例。如果行为太复杂,就完全不要使用参数化测试。用各自的测试方法表示每个负向和正向测试用例。

3.5.1 Generating data for parameterized tests

3.5.1 为参数化测试生成数据

There are some caveats in using parameterized tests (at least, in .NET) that you need to be aware of. Notice that in listing 3.11, I used the daysFromNow parameter as an input to the test method. Why not the actual date and time, you might ask? Unfortunately, the following code won’t work:

使用参数化测试时,有一些注意事项(至少在 .NET 中)需要知道。注意,在清单 3.11 中,我使用 daysFromNow 参数作为测试方法输入。你可能会问,为什么不直接使用实际日期和时间?遗憾的是,下面的代码无法工作:

[InlineData(DateTime.Now.AddDays(-1), false)]
[InlineData(DateTime.Now, false)]
[InlineData(DateTime.Now.AddDays(1), false)]
[InlineData(DateTime.Now.AddDays(2), true)]
[Theory]
public void Can_detect_an_invalid_delivery_date(
    DateTime deliveryDate,
    bool expected)
{
    DeliveryService sut = new DeliveryService();
    Delivery delivery = new Delivery
    {
         Date = deliveryDate
    };

     bool isValid = sut.IsDeliveryValid(delivery);

     Assert.Equal(expected, isValid);
}

In C#, the content of all attributes is evaluated at compile time. You have to use only those values that the compiler can understand, which are as follows:

在 C# 中,所有特性的内容都会在编译期求值。你只能使用编译器能够理解的值,包括:

The call to DateTime.Now relies on the .NET runtime and thus is not allowed.

DateTime.Now 的调用依赖 .NET 运行时,因此不被允许。

There is a way to overcome this problem. xUnit has another feature that you can use to generate custom data to feed into the test method: [MemberData]. The next listing shows how we can rewrite the previous test using this feature.

有办法克服这个问题。xUnit 还有另一个功能,可以用来生成自定义数据并送入测试方法:[MemberData]。下一个清单展示了如何使用这个功能改写前面的测试。

Listing 3.13

MemberData accepts the name of a static method that generates a collection of input data (the compiler translates nameof(Data) into a "Data" literal). Each element of the collection is itself a collection that is mapped into the two input parameters: deliveryDate and expected. With this feature, you can overcome the compiler’s restrictions and use parameters of any type in the parameterized tests.

MemberData 接受一个静态方法的名称,该方法生成输入数据集合(编译器会把 nameof(Data) 转换为 "Data" 字面量)。集合中的每个元素本身也是一个集合,会映射到两个输入参数:deliveryDateexpected。借助这个功能,你可以克服编译器限制,并在参数化测试中使用任意类型的参数。