wip-3.4
zero3.4 Naming a unit test
3.4 单元测试如何命名
It’s important to give expressive names to your tests. Proper naming helps you understand what the test verifies and how the underlying system behaves.
给测试起有表达力的名字很重要。恰当命名有助于你理解测试验证了什么,以及底层系统如何表现。
So, how should you name a unit test? I’ve seen and tried a lot of naming conventions over the past decade. One of the most prominent, and probably least helpful, is the following convention:
那么,应该如何命名单元测试?过去十年里,我见过并尝试过许多命名约定。其中最著名,也可能最没帮助的一种,是下面这个约定:
[MethodUnderTest]_[Scenario]_[ExpectedResult]
where:
其中:
MethodUnderTestis the name of the method you are testing.
MethodUnderTest是你正在测试的方法名。Scenariois the condition under which you test the method.
Scenario是你测试该方法时所处的条件。ExpectedResultis what you expect the method under test to do in the current scenario.
ExpectedResult是你期望被测方法在当前场景中做出的结果。
It’s unhelpful specifically because it encourages you to focus on implementation details instead of the behavior.
它之所以没帮助,正是因为它鼓励你关注实现细节,而不是行为。
Simple phrases in plain English do a much better job: they are more expressive and don’t box you in a rigid naming structure. With simple phrases, you can describe the system behavior in a way that’s meaningful to a customer or a domain expert. To give you an example of a test titled in plain English, here’s the test from listing 3.5 once again:
普通英文短语效果要好得多:它们表达力更强,也不会把你限制在僵硬的命名结构中。使用简单短语,你可以用对客户或领域专家有意义的方式描述系统行为。为了给出一个用普通英文命名测试的例子,下面再次展示清单 3.5 中的测试:
public class CalculatorTests
{
[Fact]
public void Sum_of_two_numbers()
{
double first = 10;
double second = 20;
var sut = new Calculator();
double result = sut.Sum(first, second);
Assert.Equal(30, result);
}
}How could the test’s name (Sum_of_two_numbers) be rewritten using the [MethodUnderTest]_[Scenario]_[ExpectedResult] convention? Probably something like this:
如果使用 [MethodUnderTest]_[Scenario]_[ExpectedResult] 约定,测试名 Sum_of_two_numbers 可以如何改写?可能会像这样:
public void Sum_TwoNumbers_ReturnsSum()The method under test is Sum, the scenario includes two numbers, and the expected result is a sum of those two numbers. The new name looks logical to a programmer’s eye, but does it really help with test readability? Not at all. It’s Greek to an uninformed person. Think about it: Why does Sum appear twice in the name of the test? And what is this Returns phrasing all about? Where is the sum returned to? You can’t know.
被测方法是 Sum,场景包含两个数字,预期结果是这两个数字之和。新名称在程序员看来似乎合乎逻辑,但它真的有助于测试可读性吗?完全没有。对不了解背景的人来说,它像天书。想想看:为什么 Sum 在测试名中出现了两次?Returns 这种表述又是什么意思?和被返回到哪里?你无法知道。
Some might argue that it doesn’t really matter what a non-programmer would think of this name. After all, unit tests are written by programmers for programmers, not domain experts. And programmers are good at deciphering cryptic names—it’s their job!
有人可能会争辩说,非程序员如何看待这个名字并不重要。毕竟,单元测试是程序员为程序员编写的,不是为领域专家写的。而程序员擅长破译晦涩名称——这就是他们的工作!
This is true, but only to a degree. Cryptic names impose a cognitive tax on everyone, programmers or not. They require additional brain capacity to figure out what exactly the test verifies and how it relates to business requirements. This may not seem like much, but the mental burden adds up over time. It slowly but surely increases the maintenance cost for the entire test suite. It’s especially noticeable if you return to the test after you’ve forgotten about the feature’s specifics, or try to understand a test written by a colleague. Reading someone else’s code is already difficult enough—any help understanding it is of considerable use.
这在一定程度上是对的,但也仅限于一定程度。晦涩名称会对每个人征收认知税,无论是不是程序员。它们需要额外脑力来弄清楚测试到底验证了什么,以及它如何与业务需求相关。这看起来可能不算什么,但心理负担会随着时间累积。它会缓慢但确定地增加整个测试套件的维护成本。当你在忘记功能细节之后回到这个测试,或尝试理解同事写的测试时,这一点尤其明显。阅读别人的代码已经足够困难,任何有助于理解的帮助都很有价值。
Here are the two versions again:
下面再次列出这两个版本:
public void Sum_of_two_numbers()
public void Sum_TwoNumbers_ReturnsSum()The initial name written in plain English is much simpler to read. It is a down-to-earth description of the behavior under test.
最初用普通英文写出的名字要容易读得多。它是对被测行为朴素直接的描述。
3.4.1 Unit test naming guidelines
3.4.1 单元测试命名指南
Adhere to the following guidelines to write expressive, easily readable test names:
遵循以下指南,可以写出有表达力且容易阅读的测试名称:
- Don’t follow a rigid naming policy. You simply can’t fit a high-level description of a complex behavior into the narrow box of such a policy. Allow freedom of expression.
不要遵循僵硬的命名策略。你根本无法把复杂行为的高层描述塞进这种策略的狭窄盒子里。允许表达自由。 - Name the test as if you were describing the scenario to a non-programmer who is familiar with the problem domain. A domain expert or a business analyst is a good example.
像向一个熟悉问题领域的非程序员描述场景一样命名测试。领域专家或业务分析师就是很好的例子。 - Separate words with underscores. Doing so helps improve readability, especially in long names.
用下划线分隔单词。这有助于提升可读性,尤其是在长名称中。
Notice that I didn’t use underscores when naming the test class, CalculatorTests. Normally, the names of classes are not as long, so they read fine without underscores.
注意,我在命名测试类 CalculatorTests 时没有使用下划线。通常类名不会那么长,所以不使用下划线也很容易阅读。
Also notice that although I use the pattern [ClassName]Tests when naming test classes, it doesn’t mean the tests are limited to verifying only that class. Remember, the unit in unit testing is a unit of behavior, not a class. This unit can span across one or several classes; the actual size is irrelevant. Still, you have to start somewhere. View the class in [ClassName]Tests as just that: an entry point, an API, using which you can verify a unit of behavior.
还要注意,虽然我使用 [ClassName]Tests 模式命名测试类,但这并不意味着测试只限于验证那个类。记住,单元测试中的单元是行为单元,而不是类。这个单元可以跨越一个或多个类;实际大小并不重要。不过,你总得从某处开始。把 [ClassName]Tests 中的类仅仅看作一个入口点、一个 API,通过它你可以验证某个行为单元。
3.4.2 Example: Renaming a test toward the guidelines
3.4.2 示例:按指南重命名测试
Let’s take a test as an example and try to gradually improve its name using the guidelines I just outlined. In the following listing, you can see a test verifying that a delivery with a past date is invalid. The test’s name is written using the rigid naming policy that doesn’t help with the test readability.
我们以一个测试为例,尝试使用刚刚概述的指南逐步改进它的名称。在下面清单中,你可以看到一个验证过去日期的配送无效的测试。该测试名称使用了僵硬命名策略,而这种策略无助于测试可读性。
This test checks that DeliveryService properly identifies a delivery with an incorrect date as invalid. How would you rewrite the test’s name in plain English? The following would be a good first try:
这个测试检查 DeliveryService 是否能正确把日期不正确的配送识别为无效。你会如何用普通英文重写这个测试名?下面是一个不错的初次尝试:
public void Delivery_with_invalid_date_should_be_considered_invalid()Notice two things in the new version:
注意新版本中的两点:
- The name now makes sense to a non-programmer, which means programmers will have an easier time understanding it, too.
这个名字现在对非程序员也有意义,这意味着程序员理解它也会更容易。 - The name of the SUT’s method—
IsDeliveryValid—is no longer part of the test’s name.
SUT 方法名IsDeliveryValid不再是测试名的一部分。
The second point is a natural consequence of rewriting the test’s name in plain English and thus can be easily overlooked. However, this consequence is important and can be elevated into a guideline of its own.
第二点是用普通英文重写测试名的自然结果,因此很容易被忽略。然而,这个结果很重要,可以上升为一条独立指南。
Method under test in the test’s name
测试名中的被测方法
Don’t include the name of the SUT’s method in the test’s name.
不要在测试名中包含 SUT 的方法名。
Remember, you don’t test code, you test application behavior. Therefore, it doesn’t matter what the name of the method under test is. As I mentioned previously, the SUT is just an entry point: a means to invoke a behavior. You can decide to rename the method under test to, say, IsDeliveryCorrect, and it will have no effect on the SUT’s behavior. On the other hand, if you follow the original naming convention, you’ll have to rename the test.
记住,你测试的不是代码,而是应用行为。因此,被测方法叫什么并不重要。如前所述,SUT 只是一个入口点:一种调用行为的手段。你可以决定把被测方法重命名为 IsDeliveryCorrect,这不会对 SUT 行为产生任何影响。另一方面,如果你遵循原来的命名约定,就必须重命名测试。
This once again shows that targeting code instead of behavior couples tests to that code’s implementation details, which negatively affects the test suite’s maintainability. More on this issue in chapter 5.
这再次表明,瞄准代码而不是行为,会把测试耦合到该代码的实现细节,从而负面影响测试套件的可维护性。第 5 章会进一步讨论这个问题。
The only exception to this guideline is when you work on utility code. Such code doesn’t contain business logic—its behavior doesn’t go much beyond simple auxiliary functionality and thus doesn’t mean anything to business people. It’s fine to use the SUT’s method names there.
这条指南唯一的例外,是你处理工具代码时。这类代码不包含业务逻辑——它的行为基本不会超出简单辅助功能,因此对业务人员没有意义。在这种情况下,使用 SUT 方法名是可以的。
But let’s get back to the example. The new version of the test’s name is a good start, but it can be improved further. What does it mean for a delivery date to be invalid, exactly? From the test in listing 3.10, we can see that an invalid date is any date in the past. This makes sense—you should only be allowed to choose a delivery date in the future.
回到这个例子。新版测试名是一个好的开始,但还可以进一步改进。配送日期无效到底是什么意思?从清单 3.10 的测试可以看到,无效日期是任何过去的日期。这是合理的——你应该只能选择未来的配送日期。
So let’s be specific and reflect this knowledge in the test’s name:
因此,我们具体一点,把这个知识反映到测试名中:
public void Delivery_with_past_date_should_be_considered_invalid()This is better but still not ideal. It’s too verbose. We can get rid of the word considered without any loss of meaning:
这更好,但仍不理想。它太啰嗦了。我们可以去掉 considered 这个词,而不损失任何含义:
public void Delivery_with_past_date_should_be_invalid()The wording should be is another common anti-pattern. Earlier in this chapter, I mentioned that a test is a single, atomic fact about a unit of behavior. There’s no place for a wish or a desire when stating a fact. Name the test accordingly—replace should be with is:
should be 这种措辞是另一种常见反模式。本章前面提到过,测试是关于一个行为单元的单个原子事实。陈述事实时,不应该出现愿望或期望。相应地命名测试——把 should be 替换为 is:
public void Delivery_with_past_date_is_invalid()And finally, there’s no need to avoid basic English grammar. Articles help the test read flawlessly. Add the article a to the test’s name:
最后,没有必要避开基本英文语法。冠词有助于让测试读起来更流畅。给测试名加上冠词 a:
public void Delivery_with_a_past_date_is_invalid()There you go. This final version is a straight-to-the-point statement of a fact, which itself describes one of the aspects of the application behavior under test: in this particular case, the aspect of determining whether a delivery can be done.
这样就完成了。最终版本是一个直截了当的事实陈述,它描述了被测应用行为的一个方面:在这个特定例子中,就是判断配送是否可以进行这一方面。