感觉 TDD 更多的是从写测例的角度出去去思考自己要做的事情,需要如何实现更完备的开发代码。包括:
然后继续设计新的测例,重复上述 3 个步骤。
书中说到,在 TDD 上述步骤的每一小步,必须可以回答一下的问题:
soundex 算法是一种语音算法,这个算法遵循以下 4 种规则:
- 保留第一个字母,丢掉所有出现的a、e、i、o、u、y、h、w
- 以数字来代替辅音(第一个字母除外):
b f p v -> 1
c g j k q s x z -> 2
d t -> 3
l -> 4
m n -> 5
r -> 6- 对于相邻的重复的数字只保留一个,即相邻的两个被替换为同一个数字的字母只保留一个; 如果出现两个编码相同的字母,且它们被 h 和 w 隔开,也这样处理;但如果被元音隔开,就要编码两次。这条规则同样适用于第一个字母。
- 当得到一个字母和单个数字时,停止处理。如果需要,补零以对齐。
想要实现上述功能,如果是你,你将如何设计第一个测例呢?
下面是作者给的步骤:
当前无任何开发代码,那么我需要一个Soundex类来实现需要的 Soundex 算法,直接用 Soundex 初始化一个对象,这个测例必然是会挂的(步骤1);为了使这个测试快速通过,我肯定是需要创建一个 Soundex 类(步骤2)
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
}
class Soundex {};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
}
ok,完成上述代码测例就可以通过了。然后我们就可以编译运行 gtest,确认测例通过了。我们已经迈出了 TDD 的伟大第一步了!
3. 设计下一个测例
有了上面的结构,可以开始思考对这个类的行为该设计怎样的接口了,类的核心就是要用来编码我们的输入,所以要先测试一个 encode 接口,首先编码一个字母。
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
auto encoded = soundex.encode("A"); // 新增代码
}
这时类还没有声明和实现 encode 接口,所以编译测例就会挂掉(步骤1),这时我们根据规则, 来先实现 encode 接口,使编译先通过:
class Soundex {
public: // 新增代码
std::string encode(const std::string& word) const {. // 新增代码
return ""; // 新增代码
} // 新增代码
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
auto encoded = soundex.encode("A"); // 新增代码
}
这样之后我们可以在此编译,这次测例可以顺利编译了,是时候验证一些有用的东西了。
class Soundex {
public:
std::string encode(const std::string& word) const {
return "";
}
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
auto encoded = soundex.encode("A");
ASSERT_THAT(encoded, testing::Eq("A")); // 新增代码
}
当前的逻辑当然不满足这个条件,运行直接报错,提示 “” 不等于 “A”
,为了让测例通过我们就要修改 encode 接口的代码,可以直接把 return "";
修改为 return "A";
,但是这仅仅只支持一个特定的单字母, return word;
岂不是更好?
class Soundex {
public:
std::string encode(const std::string& word) const {
return word; // 新增代码
}
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
auto encoded = soundex.encode("A");
ASSERT_THAT(encoded, testing::Eq("A"));
}
这样测例又可以通过了。经过上面那么多看似没必要的描述过程,正是 TDD 的核心所在 —— 增量性。 第一次接触会觉着这样的方式有点不自然而且速度很慢,但是随着试驾你的推移,小步的增量开发反而能够加快你的速度! 开发代码到这里完全还没有 Soundex 算法 4 个规则的影子。
增量的开发思路可以使我们以任何顺序一点点地开发系统,并持续地验证,向前推进。所以这时我们可以开始为新的行为写一个新的测试,并修改已有的测例来满足规范了。根据规则 4,新的测例如下:
TEST(SoundexEncoding, PadWithZerosToEnsureThreeDigits) { // 新增测例
Soundex soundex;
auto encoded = soundex.encode("I");
ASSERT_THAT(encoded, testing::Eq("I000"));
}
直接运行新增测例会挂掉,会提示 encode 返回的是 “I”
而不是 “I000”
,修改当前代码很简单,先采用硬编码:
std::string encode(const std::string& word) const {
return word + “000”; // 新增代码
}
新增示例可以通过,但之前的示例 1 就会挂掉,而让示例 1 通过的方式也很简单,把ASSERT_THAT(encoded, testing::Eq("A"));
变成 ASSERT_THAT(encoded, testing::Eq("A000"));
就可以了。
这时示例1 和示例 2 大体相同,只是数据不同,但是没关系,因为每个测试分别描述了一种行为(这就是所谓的测试代码成为文档),保留下来不仅确保系统按预期工作,还可以让每个人知道所有既定的系统行为。
是时候重构了。
主要重构的点有以下几点:
重构前:
class Soundex {
public:
std::string encode(const std::string& word) const {
return word + “000”;
}
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
Soundex soundex;
auto encoded = soundex.encode("A");
ASSERT_THAT(encoded, testing::Eq("A000"));
}
TEST(SoundexEncoding, PadWithZerosToEnsureThreeDigits) {
Soundex soundex;
auto encoded = soundex.encode("I");
ASSERT_THAT(encoded, testing::Eq("I000"));
}
重构后,为了代码干净,可以把开发代码,也就是 Soundex 类的定义与测例文件分开成两个文件了:
#include "Soundex.h"
using namespace testing;
class SoundexEncoding : public Test {
public:
Soundex soundex;
}
TEST_F(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
ASSERT_THAT(soundex.encode("A"), Eq("A000"));
}
TEST_F(SoundexEncoding, PadWithZerosToEnsureThreeDigits) {
ASSERT_THAT(soundex.encode("A"), Eq("I000"));
}
Soundex.h
#include
class Soundex {
public:
std::string encode(const std::string& word) const {
return zeroPad(word);
}
private:
std::string zeroPad(const std::string& word) const {
return word + “000”;
}
};
基本上上述的内容是 TDD 流程的一个缩影,包含了 TDD 的核心三步,可以让读者有了一个大概的了解,当然我们目前只是处理了规则 4 的一部分,还有后续的规则需要处理,文章太长会影响阅读体验,所以如果仅仅是想要对 TDD 有一个大概了解,读到这里就足够了,下面的文章会继续我们的 Soundex 类的开发之旅。
C++ 测试驱动开发 TDD(二)
原书中还有关于如何编写高质量测例的内容,以及如何处理对别人遗留代码进行测试,重构的相关内容,感兴趣的读者可以阅读原文或者留言需要博客概括。水平有限,欢迎留言交流学习。