C++ 测试驱动开发 TDD(二)

前言

本文与前篇文章紧密相连,是 TDD 的测例进一步补充和说明,建议先阅读前一篇文章有了一定的背景再继续阅读下面的内容:

C++ 测试驱动开发 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 - 1break;
       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 - 1break;
       ifencodedDigit(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 - 1break;
	      ifencodedDigit(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 的更多内容,可以直接阅读原书,比本文详尽的多。

原书中还有关于如何编写高质量测例的内容,以及如何处理对别人遗留代码进行测试,重构的相关内容,感兴趣的读者可以阅读原文或者留言需要博客概括。水平有限,欢迎留言交流学习。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
0.基础的基础 |-学习WIN64驱动开发的硬件准备 |-配置驱动开发环境 ------------------------------ 1.驱动级HelloWorld |-配置驱动测试环境 |-编译和加载内核HelloWorld ------------------------------ 2.内核编程基础 |-WIN64内核编程的基本规则 |-驱动程序与应用程序通信 |-内核里使用内存 |-内核里操作字符串 |-内核里操作文件 |-内核里操作注册表 |-内核里操作进线程 |-驱动里的其它常用代码 ------------------------------ 3.内核HOOK与UNHOOK |-系统调用、WOW64与兼容模式 |-编程实现突破WIN7的PatchGuard |-系统服务描述表结构详解 |-SSDT HOOK和UNHOOK |-SHADOW SSDT HOOK和UNHOOK |-INLINE HOOK和UNHOOK ------------------------------ 4.无HOOK监控技术 |-无HOOK监控进线程启动和退出 |-无HOOK监控模块加载 |-无HOOK监控注册表操作 |-无HOOK监控文件操作 |-无HOOK监控进线程句柄操作 |-使用对象回调监视文件访问 |-无HOOK监控网络访问 |-无HOOK监视修改时间 ------------------------------ 5.零散内容 |-驱动里实现内嵌汇编 |-DKOM隐藏进程+保护进程 |-枚举和隐藏内核模块 |-强制结束进程 |-强制读写进程内存 |-枚举消息钩子 |-强制解锁文件 |-初步探索PE32+格式文件 ------------------------------ 6.用户态HOOK与UNHOOK |-RING3注射DLL到系统进程 |-RING3的INLINE HOOK和UNHOOK |-RING3的EAT HOOK和IAT HOOK ------------------------------ 7.反回调 |-枚举与删除创建进线程回调 |-枚举与删除加载映像回调 |-枚举与删除注册表回调 |-枚举与对抗MiniFilter |-枚举与删除对象回调
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值