每周技巧 #122:测试夹具、清晰性和数据流
本节阅读量:
本文翻译自 Abseil 官网的 Tip of the Week #122: Test Fixtures, Clarity, and Dataflow。
原文最初作为 totw/122 发布于 2016 年 8 月 30 日。
作者:Titus Winters
更新于 2017 年 10 月 20 日。
快捷链接:abseil.io/tips/122
要晦涩,也要晦涩得清楚。 —— E.B. White
测试代码和生产代码有什么不同?其中一点是:测试本身没有测试。如果你写出一团分散在多个文件里、带着数百行 SetUp 的意大利面式测试代码,别人怎么能确定这个测试真的在测试它应该测试的东西?太多时候,代码评审者只能假设这些准备工作是合理的,最多对每个单独测试用例的逻辑做抽查。遇到这种情况,某些东西变化时测试很可能会失败,但很少能清楚看出变化的是否正是应该被测试捕捉到的东西。
反过来,如果你让每个测试尽量简单、直接,那么别人就更容易通过阅读确认它是正确的,理解其中逻辑,并以更高质量评审测试逻辑。下面看几种达成这一点的简单办法。
夹具中的数据流
考虑下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class FrobberTest : public ::testing::Test {
protected:
void ConfigureExampleA() {
example_ = "Example A";
frobber_.Init(example_);
expected_ = "Result A";
}
void ConfigureExampleB() {
example_ = "Example B";
frobber_.Init(example_);
expected_ = "Result B";
}
Frobber frobber_;
string example_;
string expected_;
};
TEST_F(FrobberTest, CalculatesA) {
ConfigureExampleA();
string result = frobber_.Calculate();
EXPECT_EQ(result, expected_);
}
TEST_F(FrobberTest, CalculatesB) {
ConfigureExampleB();
string result = frobber_.Calculate();
EXPECT_EQ(result, expected_);
}
|
在这个相当简单的例子里,测试横跨了 30 行代码。很容易想象不那么简单的例子会膨胀到 10 倍长:肯定已经不是单个屏幕能放下的。想验证代码正确性的读者或代码评审者必须来回扫视:
- “好,这是一个
FrobberTest,它定义在哪里……哦,在这个文件里。很好。”
- “
ConfigureExampleA……这是 FrobberTest 的方法。它在操作一些成员变量。那些变量是什么类型?怎么初始化的?好,一个 Frobber 和两个字符串。有 SetUp 吗?好,默认构造。”
- “回到测试:好,我们计算一个结果,然后和
expected_ 比较……那里之前又存了什么?”
把它和用更简单风格写出的等价代码比较一下:
1
2
3
4
5
6
7
8
9
10
11
|
TEST(FrobberTest, CalculatesA) {
Frobber frobber;
frobber.Init("Example A");
EXPECT_EQ(frobber.Calculate(), "Result A");
}
TEST(FrobberTest, CalculatesB) {
Frobber frobber;
frobber.Init("Example B");
EXPECT_EQ(frobber.Calculate(), "Result B");
}
|
采用这种风格,即使有数百个测试,我们也能只凭局部信息准确看出正在发生什么。
优先使用自由函数
在前一个例子中,所有变量初始化都很简洁。真实测试里并不总是这样。不过,关于数据流和避免夹具的同一组想法仍然适用。考虑这个 protobuf 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class BobberTest : public ::testing::Test {
protected:
void SetUp() override {
bobber1_ = PARSE_TEXT_PROTO(R"(
id: 17
artist: "Beyonce"
when: "2012-10-10 12:39:54 -04:00"
price_usd: 200)");
bobber2_ = PARSE_TEXT_PROTO(R"(
id: 21
artist: "The Shouting Matches"
when: "2016-08-24 20:30:21 -04:00"
price_usd: 60)");
}
BobberProto bobber1_;
BobberProto bobber2_;
};
TEST_F(BobberTest, UsesProtos) {
Bobber bobber({bobber1_, bobber2_});
SomeCall();
EXPECT_THAT(bobber.MostRecent(), EqualsProto(bobber2_));
}
|
同样,集中化的重构带来了大量间接性:声明和初始化被分开,而且可能离实际使用很远。更进一步,由于中间有 SomeCall(),又因为我们使用了夹具和夹具成员变量,除非检查 SomeCall() 的细节,否则无法确定 bobber1_ 和 bobber2_ 在初始化之后、EXPECT_THAT 验证之前没有被修改。你很可能还得继续来回滚动。
换成下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
BobberProto RecentCheapConcert() {
return PARSE_TEXT_PROTO(R"(
id: 21
artist: "The Shouting Matches"
when: "2016-08-24 20:30:21 -04:00"
price_usd: 60)");
}
BobberProto PastExpensiveConcert() {
return PARSE_TEXT_PROTO(R"(
id: 17
artist: "Beyonce"
when: "2012-10-10 12:39:54 -04:00"
price_usd: 200)");
}
TEST(BobberTest, UsesProtos) {
Bobber bobber({PastExpensiveConcert(), RecentCheapConcert()});
SomeCall();
EXPECT_THAT(bobber.MostRecent(), EqualsProto(RecentCheapConcert()));
}
|
把初始化移到自由函数中,可以清楚表明不存在隐藏的数据流。如果辅助函数名字选得好,你很可能不用向上滚动查看辅助函数细节,也能评审这个测试是否正确。
五个简单步骤
通常可以通过下面这些步骤提升测试清晰性:
- 合理时避免夹具。有时确实避不开。
- 如果正在使用夹具,尽量避免夹具成员变量。它们太容易以类似全局变量的方式被操作:由于夹具中的任何代码路径都可能修改成员,数据流会变得很难追踪。
- 如果有些变量需要复杂初始化,直接写在每个测试里会让测试难读,可以考虑使用一个辅助函数(不是夹具的一部分)来记录这种初始化,并直接返回对象。
- 如果必须使用包含成员变量的夹具,尽量避免让方法直接操作这些成员:只要可能,就把它们作为参数传入,以便让数据流清楚。
- 尝试先写测试,再写头文件:如果一开始就从易于测试的用法出发,你的 API 通常会更好,测试也几乎总是更清楚。
每周技巧 #123:`absl::optional` 和 `std::unique_ptr`
下一节