你在错误命名你的测试用例!
https://enterprisecraftsmanship.com/posts/you-naming-tests-wrong/
提供表述性的名称很重要。正确的命名有助于理解测试用例在验证什么,以及底层系统的行为。本文中,我们将分析一个常见的,较差的命名约定,并学习如何改进它。
所以,你是如何命名单元测试用例的?在过去的几十年中,我见过并且尝试过各种的命名规范。最为突出的,也可能是最没有帮助的,就是下面的命名规范。
[MethodUnderTest]_[Scenario]_[ExpectedResult]
这里面的:
- MethodUnderTest 表示测试的目标方法名
- Scenario 测试方法所使用的条件
- ExpectedResult 在当前条件下,你期望测试方法返回的结果
它并没有帮助!因为它鼓励你关注于实现的细节而不是其行为。反过来说,简单的英文句子可以做的更好。这样更有表述性,还不会把你束缚在僵化的命名规则上。使用简单的句子,你可以使用对使用人或者领域专家更有意义的方式来描述系统的行为。
让我们看一个示例,( sut 表示测试目标 )
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);
}
}
如果使用 [MethodUnderTest][Scenario][ExpectedResult] 模式,那么该测试用例的名称可能应该是如下形式:
public void Sum_TwoNumbers_ReturnsSum()
这是因为测试的目标方法是 Sum,测试的场景中包含了 2 个数字,并且测试的目标是 Sum 方法的返回结果应该是两数之和。
该新名称从名称上符合程序员的眼光,但是它真的有助于测试用例的可读性吗?一点也没有,对于不知情的人来说,后面的版本就是不知所云。考虑一下。为什么这里会有 2 个 Sum 单词在命名中呢?而这个 Return 又是什么呢?Sum 返回到哪里去?你不知道。
有种反对的声音是,对于非程序员如何考虑该名称无关紧要。不管怎样,单元测试是由程序员面向程序员编写的,而不是领域专家,并且程序员很善于理解这些玄幻的命名 - 这就是他们的工作!
确实如此,但仅仅是某种程度而已。奇怪的名称对所有人强加了智商税,无论是否针对程序员。这需要额外的脑力来理解该测试到底要验证什么,以及它相关联的业务需求。或许看起来只有一点,但伴随时间的增长逐渐成为负担。它缓慢,但确实增加了维护整个测试集的代价。需要特别指出的是,当你遗忘了关于这个功能的规范后,回头再看该测试用例,或者试图理解同事开发的测试用例的时候。阅读别人的代码已经够难的了。 有助于理解它的任何帮助都非常有用。
初始的纯英文版本要看起来简单了一点。直接描述测试的行为。下面是两个版本的对比:
public void Sum_of_two_numbers()
public void Sum_TwoNumbers_ReturnsSum()
单元测试的命名规则
使用下面的规则来编写表述性、更易读的测试用例名称:
- 非僵化命名策略,您根本无法将复杂行为的高级描述放入此类策略的受限的框中。 允许自由命名。
- 假设你向该问题领域的非程序员描述该行为的方式命名测试用例,领域专家或者业务分析师是不错的考虑对象。
- 使用下划线分隔单词,这有助于改进可读性,尤其是比较长的命名。
注意我在类名中,我并没有使用下划线,类名通常不会太长,所以它不需要使用下划线。
还有就是,尽管我使用了 [ClassName]Tests 模式来命名测试类名,但这不意味着测试的对象只限于该 ClassName。单元测试中的单元是行为单元,而不是类。测试单元可以跨越单个或者多个类,实际的大小无关紧要。不过,不过,你必须从某个地方开始。 仅将 [ClassName]Tests 中的类视为:一个入口点,一个 API,您可以使用它来验证一个行为单元。
示例:基于新规则重新命名
让我们使用另外一个例子,来尝试使用上面的规则来改进命名。这里是使用过去的日期来验证投递是否有效。该测试用例的名称使用神秘命名测咯,无助于测试的可读性。
[Fact]
public void IsDeliveryValid_InvalidDate_ReturnsFalse()
{
DeliveryService sut = new DeliveryService();
DateTime pastDate = DateTime.Now.AddDays(-1);
Delivery delivery = new Delivery
{
Date = pastDate
};
bool isValid = sut.IsDeliveryValid(delivery);
Assert.False(isValid);
}
该测试用例检查 DeliveryService 是否将不正确的日期视为无效的投递日期。你如何使用纯英文来重命名该测试用例呢?下面可能是不错的开始:
public void Delivery_with_invalid_date_should_be_considered_invalid()
对于新版本的名称有两点需要注意:
- 对于非程序员现在不错。这意味着对于程序员也更容易理解
- 测试的目标方法名称 IsDeliveryValid 现在已经不出现在测试用例的名称中
第 2 点是使用纯英文来重新测试用例名称的自然结果,很容易被忽视。然而,这个结果很重要,可以提升为自己的指导方针。
不要在测试用例名称中包含测试方法名称。记住,你不是在测试代码,你在测试应用程序的行为。所以,正在测试什么方法并不重要。如我前面提到的,SUT 只是测试的入口,来出发某个行为。你可以考虑重新命名被测试的方法名称,就是 IsDeliveryCorrect,这并不影响测试的行为。从另一个角度说,如果你使用原来的名称方式,改变方法名称你就必须也改变测试用例名称。这也再此证明了目标代码的实现细节被紧耦合到测试用例上,这对测试集的维护性造成负面影响。
对于该规则的一种特例是测试工具代码的时候。这些代码没有业务逻辑 - 它的行为中只有一些辅助功能,对业务人员没有意义。此时使用 SUT 测试目标的方法名称就没有问题。
但是让我们回到示例中,新的测试用例名称是个不错的起点,但还可以继续改进。投递日期无效到底是什么意思?分析测试用例,我们可以发现无效的日期可以是任何过去的日期。这可以理解 - 你只能允许选择未来的投递日期。
所以,让我们具体一点,将这些知识反映到测试用例的名称上:
public void Delivery_with_past_date_should_be_considered_invalid()
这样好了一点,但还不够理想。它太繁琐了。我们可以把单词 considered 删除掉,这不会丢失语义。
public void Delivery_with_past_date_should_be_invalid()
句子中的 should_be 是另一个常见的反模式。测试用例应该是简单的,关于行为的原子事实。对事实不应该有期望或者应该应该。相应地将 should_be 替换为 is。
public void Delivery_with_past_date_is_invalid()
最后,不应该忽视基本的英语语法。冠词有助于阅读的完美无暇。增加冠词 a 到测试用例的名称上。
public void Delivery_with_a_past_date_is_invalid()
就是这样,最终的版本直达本意,它直接描述了测试行为的一个方面。对于本例来说,就是是否可以完成投递一个方面。
总结
- 不要使用玄幻的命名测咯
- 使用面向熟悉问题的非程序员描述场景的方式,来命名测试用例
- 使用下划线分隔测试用例的名称
- 不要在测试用例名称中包含被测试的方法名称