文章目录
前言
本文与前篇文章紧密相连,是 TDD 的测例进一步补充和说明,建议先阅读前一篇文章有了一定的背景再继续阅读下面的内容:
增加 Soundex 算法测例 3 - 单字母编码
重构以后可以继续下一个测试,我们可以处理规则2(第一个字母后,用数字替换辅音),替换规则中字母 b 对应数字 1,则可以写下面这样的测例:
TEST_F(SoundexEncoding, ReplaceConstantWithAppropriateDigits) {
ASSERT_THAT(soundex.encode("Ab"), Eq("A100"));
}
最最最简单的使测例通过的修改方式还是硬编码:
std::string encode(const std::string& word) const {
if (word == "Ab") return "A100"; // 新增代码
return zeroPad(word);
}
但这并不是我们想要的,我们需要更通用的解决方案:
std::string encode(const std::string& word) const {
auto encoded = word.substr(0, 1); // 新增代码
if (word.length() > 1) // 新增代码
encoded +="1"; // 新增代码
return zeroPad(encoded);
}
这时运行测例,肯定会失败,因为输出会是A1000
而不是 A100
,因为补齐逻辑 zeroPad()
不对,必须修改它,以考虑到待编码词的长度:
std::string zeroPad(const std::string& word) const {
auto zerosNeeded = 4 - word.length();
return word + std::string(zerosNeeded,'0');
}
经过上述修改,测试可以通过了!
到这里,代码逐渐复杂起来,我们自己知道 encode 函数是如何工作的,但是其他人可能会花更多时间,下面可以来重构为更可读性的方案。
Soundex 算法测例 3 重构
class Soundex {
public:
static const size_t MaxCodeLength = 4;
std::string encode(const std::string& word) const {
return zeroPad(head(word) + encodedDigits(word));
}
private:
std::string head(const std::string& word) const {
return word.substr(0, 1);
}
std::string encodedDigits(const std::string& word) const {
if (word.length() > 1) return "1";
return "";
}
std::string zeroPad(const std::string& word) const {
auto zerosNeeded = MaxCodeLength - word.length();
return word + std::string(zerosNeeded,'0');
}
};
完善 Soundex 算法测例 3
我们继续添加另外的辅音变换测例:
TEST_F(SoundexEncoding, ReplaceConstantWithAppropriateDigits) {
EXPECT_THAT(soundex.encode("Ab"), Eq("A100"));
EXPECT_THAT(soundex.encode("Ac"), Eq("A200"));
}
上述代码我们依旧可以通过硬编码增加 if 分支来处理特殊情况。(泪目,平时开发你的 if 用的有没有很多 T _ T),但是我们需要处理的字符有很多,可以用基于hash的集合代替简单的 if 分支。
std::string encodedDigit(char letter) const {
const std::unordered_map<char, std::string> encodings {
{'b', "1"},
{'c', "2"},
{'d'. "3"} ... // 根据规则 2 完善所有的映射
};
return encodings.find(letter)->second;
}
上面都是合法字符的情况,当是非法字符呢?
增加 Soundex 算法测例 4 - 特殊字符
TEST_F(SoundexEncoding, IgnoreNonAlphabetics) {
ASSERT_THAT(soundex.encode("A#"), Eq("A000"));
}
这里 unordered_map 没有找到 ‘#’,然后我们还直接解引用,程序会直接崩溃的,所以这里需要修改相关代码:
std::string encodedDigit(char letter) const {
const std::unordered_map<char, std::string> encodings {
{'b', "1"},
{'c', "2"},
{'d'. "3"} ... // 根据规则 2 完善所有的映射
};
auto it = encodings.find(letter);
return it == encodings.end() ? "" : it->second;
}
目前我们完成的功能是转换首字母后面的第一个字母,接下来我们需要测试转化除首字母外的其他字母。
增加 Soundex 算法测例 5 - 处理完整输入
TEST_F(SoundexEncoding, ReplacesMultipleConstantWithDigits) {
ASSERT_THAT(soundex.encode("Acdl"), Eq("A234"));
}
这里使用 For 循环即可:
std::string head(const std::string& word) const {
return word.substr(0, 1);
}
std::string tail(const std::string& word) const {
return word.substr(1);
}
std::string encodedDigits(const std::string& word) const {
std::string encoding;
for (letter : word)
encoding += encodedDigit(letter);
return encoding;
}
重新测试,上述测例就可以通过了。
增加 Soundex 算法测例 6 - 限制长度
规则 4 说 Soundex 编码的结果长度必须是 4 个字符。下面为此写一个新的测试。
TEST_F(SoundexEncoding, ReplacesMultipleConstantWithDigits) {
ASSERT_THAT(soundex.encode("Dcdlb").length(), Eq(4u));
}
这时运行测例是会报错的,因为之前写的 zeroPad()
有问题,当长度大于 4 时,需要补 0 的长度时负数,创建了不合法长度的字符串,也可以改变 encodedDigits(),使之在得到足够的字母时,停止编码。
std::string encodedDigits(const std::string& word) const {
std::string encoding;
for (letter : word)
if(encoding.length() == MaxCodeLength - 1) break;
encoding += encodedDigit(letter);
return encoding;
}
增加 Soundex 算法测例 7 - 删除元音
规则1说需要丢掉所有的元音以及 w、h 和 y。
TEST_F(SoundexEncoding, IgnoreVowelLikeLetters) {
ASSERT_THAT(soundex.encode("Baeiouhycdl"), Eq("B234"));
}
这时测例运行直接就通过了,可以问自己一个问题:“我在测试中做了与期望不一致的事情吗?”
测例提前通过的原因可能是因为之前的代码写的步伐有一点点大,可以回望看一下通过的原因是 map 搜索不到元音字母就会返回空字符串,我们本可以选择搜索不到的字母就返回相同的字母,而非空字符串。
增加 Soundex 算法测例 8 - 处理重复字符
接下来就要处理规则 2 中的前后重复的情况。
TEST_F(SoundexEncoding, CombinesDuplicateEncodings) {
ASSERT_THAT(soundex.encode("Abfcgdt"), Eq("A123"));
}
为了让测例 8 通过,可以引入一个局部变量,记录最后一个追加的数字,并在每次循环迭代时更新它,变量还是有点太模棱两可了,进一步,可以专门写一个函数来做这件事。
std::string encodedDigits(const std::string& word) const {
std::string encoding;
for (letter : word)
if(encoding.length() == MaxCodeLength - 1) break;
if(encodedDigit(letter) != lastDigit(encoding)) // 新增代码
encoding += encodedDigit(letter);
return encoding;
}
std::string lastDigit(const std::string& encoding) const {
if (encoding.empty()) return "";
reeturn std::string(1, encoding.back());
}
最后根据前面的 8 个测试,当前 Soundex 的完整代码如下:
#include <string>
#include <unordered_map>
class Soundex {
public:
static const size_t MaxCodeLength = 4;
std::string encode(const std::string& word) const {
return zeroPad(head(word) + encodedDigits(word));
}
private:
std::string encodedDigit(char letter) const {
const std::unordered_map<char, std::string> encodings {
{'b', "1"},
{'c', "2"},
{'d'. "3"} ... // 根据规则 2 完善所有的映射
};
auto it = encodings.find(letter);
return it == encodings.end() ? "" : it->second;
}
std::string head(const std::string& word) const {
return word.substr(0, 1);
}
std::string tail(const std::string& word) const {
return word.substr(1);
}
std::string encodedDigits(const std::string& word) const {
std::string encoding;
for (letter : word)
if(encoding.length() == MaxCodeLength - 1) break;
if(encodedDigit(letter) != lastDigit(encoding)) // 新增代码
encoding += encodedDigit(letter);
return encoding;
}
std::string lastDigit(const std::string& encoding) const {
if (encoding.empty()) return "";
reeturn std::string(1, encoding.back());
}
std::string zeroPad(const std::string& word) const {
auto zerosNeeded = MaxCodeLength - word.length();
return word + std::string(zerosNeeded,'0');
}
};
后记
后面还有测例需要添加,例如输入第一个字母为小写时,被元音分割时,输入字符串含有分隔符该怎么办?是忽略还是抛出异常?怎么处理非英语字母中的辅音等。。。
根据上面的 8 个测例,一步一步,从一个空的类,逐渐经过 TDD 的 3 个步骤,丰满了起来,并且写出的代码比较干净,容易阅读,测试驱动开发。
如果阅读能慢慢跟到这里,基本已经对 TDD 有了更多的了解和感受,本文的写作目的就达到了,再往后想了解关于 TDD 的更多内容,可以直接阅读原书,比本文详尽的多。
原书中还有关于如何编写高质量测例的内容,以及如何处理对别人遗留代码进行测试,重构的相关内容,感兴趣的读者可以阅读原文或者留言需要博客概括。水平有限,欢迎留言交流学习。