Test-Drived Development

测试驱动开发三步曲:写一个失败的测试用例->编写生产代码通过这个测试用例(transformation)->重构(refactor)。重构是指不改变程序的外在行为的前提下消除代码坏味道,目前已有不少的指导书籍。而第二步变形(Transformation) 编写生产代码通过测试用例,这是TDD三个环节中最困难的,有时甚至会陷入僵局。

Transformation Priority Premise

变形(Transformation)的困难在于:如果步子太大,会花费很长时间才能通过测试;如果实现思路不对,甚至陷入僵局(impasse)无法进一步演进。Uncle Bob在2013年提出了TPP(transformation priority premise)的方法,他认为优秀代码的演进过程不是从一个愚蠢的状态逐步转变为一个优雅的状态;而是从一个具体的状态转变成为一个通用的状态。因此他对常用的编码变形手段进行排序,优先级高的手段是更具体的手段,而低优先级的变形手段是更通用的手段。通过这个变形优先列表不仅可以很好的控制节奏,最重要的是尽量推迟通用的手段。

通过测试用例的驱动不断的深入,问题的本质会逐步浮现。当本质浮现之后,再逐步采用通用的手段演进。

TDD陷入困境往往是过早的采用通用的手段。通用的手段步子更大、更死板,有时看似更简洁,但隐藏了细节,难以进一步演进。

TPP的列表如下(1为最高优先级):

1 ({} → nil) no code at all → code that employs nil(空函数 ->返回空结果)

2 (nil → constant)(空->常量)

3 (constant → constant+) a simple constant to a more complex constant(常量->更复杂的多个常量)

4 (constant → scalar) replacing a constant with a variable or an argument(常量->变量)

5 (statement → statements) adding more unconditional statements.(单条语句->多条语句)

6 (unconditional → if) splitting the execution path(无条件语句->if分支语句)

7 (scalar → array)(变量->数组)

8 (array → container)(数组->容器)

9 (statement → tail-recursion)(语句->尾部递归)

10 (if → while)

11 (statement → non-tail-recursion)(语句->非尾部递归)

12 (expression → function) replacing an expression with a function or algorithm(表达式->函数或算法)

13 (variable → assignment) replacing the value of a variable.(变量->赋值)

14 (case) adding a case (or else) to an existing switch or if(新增case或else语句->已有的switch或if语句)

来源: https://en.wikipedia.org/wiki/Transformation_Priority_Premise


代码操练

下面以Anagrams为例进行代码操练(c++、gtest),看看TPP是否如何帮助我们走出困境。

需求描述

Write a program to generate all potential anagrams of an input string. Forexample, the potential anagrams of “biro” are

biro bior brio broi boir bori

ibro ibor irbo irob iobr iorb

rbio rboi ribo riob roib robi

obir obri oibr oirb orbi orib

对于给定的字符串,输出所有潜在字符组合。问题描述比较清晰,但如何解决?

  • 第一个测试用例:

    一个空的字符串,可能的anagrams集合也为空
  1. TEST(Anagrams, test_anagrams_of_null_should_be_null)
  2. {
  3. vector result = {};
  4. ASSERT_EQ(result, getAnagrams(""));
  5. }
  • 变形:应用最高优先级的变形手段返回一个空的结果集合 ({} →nil)
  1. vector getAnagrams(string str)
  2. {
  3. return vector();
  4. }
  • 重构:定义结果集合的数据类型Result,更好的揭示意图。
  1. typedef vector<string> Result;
  2. Result getAnagrams(string str)
  3. {
  4. return Result();
  5. }

测试用例同步重构

  1. TEST(Anagrams, test_anagrams_of_null_should_be_null)
  2. {
  3. Result result = {};
  4. ASSERT_EQ(result, getAnagrams(""));
  5. }
  • 第二个测试用例:一个字符的anagrams只有其自身
  1. TEST(Anagrams, test_anagrams_of_a_should_be_a)
  2. {
  3. Result result = {"a"};
  4. ASSERT_EQ(result, getAnagrams("a"));
  5. }
  • 变形:中优先级的变形手段(nil→ constant)、(constant → scalar)、(unconditional → if)
  1. Result getAnagrams(string str)
  2. {
  3. if (str == "")
  4. return Result();
  5. Result res = {str};
  6. return res;
  7. }
  • 第三个测试用例:两个字符的anagrams包含两个结果
  1. TEST(Anagrams, test_anagrams_of_ab_should_be_ab_ba)
  2. {
  3. Result result = {"ab", "ba"};
  4. ASSERT_EQ(result, getAnagrams("ab"));
  5. }
  • 变形 : 把两个字符进行相互颠倒,就可以搞定。

    因此按照惯性的方法继续中优先级的变形手段: (unconditional → if)
  1. Result getAnagrams(string str)
  2. {
  3. if (str == "")
  4. return Result();
  5. Result res = {str};
  6. if (str.size() == 2)
  7. res.push_back(str.substr(1, 1) + str.substr(0, 1));
  8. return res;
  9. }
  • 第四个测试用例:三个字符的anagrams包含六个结果
  1. TEST(Anagrams, test_anagrams_of_abc_should_be_abc_acb_bac_bca_cab_cba)
  2. {
  3. Result result = {"abc", "acb", "bac", "bca", "cab", "cba"};
  4. ASSERT_EQ(result, getAnagrams("abc"));
  5. }
  • 变形 or 僵局

    三个字符的问题可以转换为两个字符的问题吗?似乎可以先遍历选取第一个字符,再剩下就是已解决的两个字符的已知问题。

    但是结果如何组合在一起?需要组合一个字符和一个数组…

    步子太大了,容易陷入僵局(impasse),说好的easy模式呢?

    回顾当前思路和上一步的变形过程,过早的使用相对低优先级的手段 (unconditional → if),隐藏了包含问题本质的细节,也难以继续演进。如下面这段代码隐藏了两个字符下的anagrams规律。
  1. Result res = {str};
  2. if (str.size() == 2)
  3. res.push_back(str.substr(1, 1) + str.substr(0, 1));
  4. return res;

现在重新回到easy模式下,按照TPP的优先顺序,优选高优先级的手段(nil → constant)、(constant → constant+)

  1. Result getAnagrams(string str)
  2. {
  3. if (str.size() == 0)
  4. return Result();
  5. else if(str.size() == 1)
  6. return Result{str};
  7. else if(str.size() == 2){
  8. return Result{str,
  9. str.substr(1, 1) + str.substr(0, 1)};
  10. }
  11. }

但是第三个测试用例的问题如何解决呢?easy,用高优先级的方法吧!

  1. else if(str.size() == 2){
  2. return Result{str,
  3. str.substr(1, 1) + str.substr(0, 1)};
  4. }
  5. else{
  6. return Result{"abc", "acb", "bac", "bca", "cab", "cba"};
  7. }

呵呵,有点骗人?

但变形还没有结束,继续…

  1. else{
  2. return Result{string("a") + "bc",
  3. string("a") + "cb",
  4. "bac",
  5. "bca",
  6. "cab",
  7. "cba"};
  8. }

这个时候“bc”和“cb”是n-1规模的答案集合,当然要用(statement → tail-recursion)手段变形

  1. return Result{string("a") + getAnagrams("bc")[0],
  2. string("a") + getAnagrams("bc")[1],

“a”代表字符串的首字符,继续变形(constant → scalar)

  1. return Result{str.substr(0, 1) + getAnagrams("bc")[0],
  2. str.substr(0, 1) + getAnagrams("bc")[1],

同理其他的语句也用类似的方法变形

  1. return Result{str.substr(0, 1) + getAnagrams("bc")[0],
  2. str.substr(0, 1) + getAnagrams("bc")[1],
  3. str.substr(1, 1) + getAnagrams("ac")[0],
  4. str.substr(1, 1) + getAnagrams("ac")[1],
  5. str.substr(2, 1) + getAnagrams("ab")[0],
  6. str.substr(2, 1) + getAnagrams("ab")[1]};

“bc”、“ac”、“ab”都是代表剩下的字符串。因此在这里考虑提出一个函数,处理剩下的字符串,继续变形。

  1. return Result{str.substr(0, 1) + getAnagrams(strDelChar(str , 0))[0],
  2. str.substr(0, 1) + getAnagrams("bc")[1],
  3. str.substr(1, 1) + getAnagrams("ac")[0],
  4. str.substr(1, 1) + getAnagrams("ac")[1],
  5. str.substr(2, 1) + getAnagrams("ab")[0],
  6. str.substr(2, 1) + getAnagrams("ab")[1]};

吸取刚才的教训,strDelChar这个函数我也按照TPP的顺序变形,坚持easy模式!

  1. string strDelChar(string s, size_t pos){
  2. return "bc";
  3. }

对其他的挑剩下的字符也采用相同的方式处理

  1. return Result{str.substr(0, 1) + getAnagrams(strDelChar(str , 0))[0],
  2. str.substr(0, 1) + getAnagrams(strDelChar(str , 0))[1],
  3. str.substr(1, 1) + getAnagrams(strDelChar(str , 1))[0],
  4. str.substr(1, 1) + getAnagrams(strDelChar(str , 1))[1],
  5. str.substr(2, 1) + getAnagrams(strDelChar(str , 2))[0],
  6. str.substr(2, 1) + getAnagrams(strDelChar(str , 2))[1]};

此时的strDelChar函数变形成这样了

  1. string strDelChar(string s, size_t pos){
  2. if (pos == 0)
  3. return "bc";
  4. if (pos == 1)
  5. return "ac";
  6. return "ab";
  7. }

根据常量实际的意义继续变形(constant → scalar)

  1. string strDelChar(string s, size_t pos){
  2. if (pos == 0)
  3. return s.substr(1, 2);
  4. if (pos == 1)
  5. return s.substr(0, 1) + s.substr(2, 1);
  6. else
  7. return s.substr(0, 2);
  8. }

剩下的字符串包括两部分的内容:扣去字符前的部分、扣去字符后面的部分。因此继续变形

  1. string strDelChar(string s, size_t pos){
  2. if (pos == 0)
  3. return s.substr(0, 0) +s.substr(1, 2);
  4. if (pos == 1)
  5. return s.substr(0, 1) + s.substr(2, 1);
  6. else
  7. return s.substr(0, 2) + s.substr(2, 0);
  8. }

通过上面的代码可以看到pos就是分割点,pos位置的字符被扣去。因此可以采用更通用的方法继续变形

  1. string strDelChar(string s, size_t pos){
  2. string before = s.substr(0, pos);
  3. string after = s.substr(pos+1);
  4. return before + after;
  5. }

最后消除不必要的局部变量,搞定这个函数

  1. string strDelChar(string s, size_t pos){
  2. return s.substr(0, pos) + s.substr(pos+1);
  3. }

获取首字符的地方也可以抽取一个函数

  1. else{
  2. return Result{strGetChar(str, 0) + getAnagrams(strDelChar(str , 0))[0],
  3. strGetChar(str, 0) + getAnagrams(strDelChar(str , 0))[1],
  4. strGetChar(str, 1) + getAnagrams(strDelChar(str , 1))[0],
  5. strGetChar(str, 1) + getAnagrams(strDelChar(str , 1))[1],
  6. strGetChar(str, 2) + getAnagrams(strDelChar(str , 2))[0],
  7. strGetChar(str, 2) + getAnagrams(strDelChar(str , 2))[1]};
  8. }
  1. string strGetChar(string s, size_t pos){
  2. return s.substr(pos, 1);
  3. }

一系列变形后,函数更通用了,不仅仅处理abc的问题,所有三个字符的问题都可以解决。

继续回到刚才的主体测试函数继续变形,此处的六条语句实际上是一个遍历首字符的过程,因此抽取外层的循环:

  1. else{
  2. Result res;
  3. for (size_t i = 0; i < str.size(); i++){
  4. res.push_back(strGetChar(str, i) + getAnagrams(strDelChar(str , i))[0]);
  5. res.push_back(strGetChar(str, i) + getAnagrams(strDelChar(str , i))[1]);
  6. }
  7. return res;
  8. }

内部的两条语句本质上是遍历两个字符子问题的结果集,因此继续变形

  1. else{
  2. Result res;
  3. for (size_t i = 0; i < str.size(); i++){
  4. Result subRes = getAnagrams(strDelChar(str, i));
  5. for (auto subResStr : subRes){
  6. res.push_back(strGetChar(str, i) + subResStr);
  7. }
  8. }
  9. return res;
  10. }

再回顾一下完整的这个主体函数:

else分支部分 将规模为n的原问题分解为 遍历首字符 + 规模为n-1的问题,通过递归解决了更大规模的问题。因此现在这部分代码更加通用。

  1. Result getAnagrams(string str)
  2. {
  3. if (str.size() == 0)
  4. return Result();
  5. else if(str.size() == 1)
  6. return Result{str};
  7. else if(str.size() == 2){
  8. return Result{str.substr(0, 1) + str.substr(1, 1),
  9. str.substr(1, 1) + str.substr(0, 1)};
  10. }
  11. else{
  12. Result res;
  13. for (size_t i = 0; i < str.size(); i++){
  14. Result subRes = getAnagrams(strDelChar(str, i));
  15. for (auto subResStr : subRes)
  16. res.push_back(strGetChar(str, i) + subResStr);
  17. }
  18. return res;
  19. }
  20. }

再看一下其他的分支情况:

1 规模为0的情况是else分支的特殊情况,0不进入循环体。因此第一个if语句可以删除。

2 规模为1的情况属于递归的出口,因此第一个else if必须保留。

3 规模为2的情况也是首字符+规模1(2-1)的问题。因此规模2的问题属于else分支的一种特殊情况,也可以删除。

删除无用的代码,最终的结果:

  1. string strDelChar(string s, size_t pos){
  2. return s.substr(0, pos) + s.substr(pos + 1);
  3. }
  4. Result getAnagrams(string str)
  5. {
  6. Result res;
  7. if(str.size() == 1)
  8. return Result{str};
  9. for (size_t i = 0; i < str.size(); i++){
  10. Result subRes = getAnagrams(strDelChar(str, i));
  11. for (auto subResStr : subRes)
  12. res.push_back(str.substr(i, 1) + subResStr);
  13. }
  14. return res;
  15. }

添加最后一个四个字符的测试用例验证一下,当然是没有问题的。

  1. TEST(Anagrams, test_anagrams_of_biro)
  2. {
  3. Result result = {"biro", "bior", "brio", "broi", "boir", "bori",
  4. "ibro", "ibor", "irbo", "irob", "iobr", "iorb",
  5. "rbio", "rboi", "ribo", "riob", "robi", "roib",
  6. "obir", "obri", "oibr", "oirb", "orbi", "orib"};
  7. ASSERT_EQ(result, getAnagrams("biro"));
  8. }

总结

通过实际的代码操练,体会了TPP的价值。在编程的初期,尽量使用一些具体的手段(高优先级),这样可以最大的保留问题的细节。随着TDD的深入,问题的本质会逐步的自动暴露出来。此时才采用一些更通用的的手段(低优先级)描述问题的本质。这种方式不仅可以控制变形的节奏,也帮助开发人员理解问题的本质,避免陷入僵局。

最后补充Uncle Bob关于TPP实施的注意事项:

  1. When passing a test, preferhigher priority transformations.

    (在编写生产代码时,优选高优先级变形手段)
  2. When posing a test chooseone that can be passed with higher priority transformations.

    (在编写测试用例时,优选可用高优先变形手段解决的测试用例)
  3. When an implementationseems to require a low priority transformation, backtrack to see if there is asimpler test to pass.

    (当需要使用低优先级手段时,回头看看是否有更简单的测试用例)

用TPP开启TDD的easy模式的更多相关文章

  1. 电脑知识,一键开启Win10“超级性能模式”

    现在主流系统以及从WIN7慢慢的转移到了WIN10,微软也为WIN10做了很多优化跟更新.今天要跟大家说的这个功能很多人肯定没有听说过.那就是WIN10的超级性能模式. 1. 大多数Win10是没有滴 ...

  2. libcurl使用easy模式阻塞卡死等问题的完美解决

    引言: 由于要在android手机测进行DM开发, 其中最重要的就是FUMO和SCOMO下载, 下载使用的是linux开源库libcurl. 于是就把libcurl的使用研究了一遍, 有些心得, 并解 ...

  3. 强制开启android webview debug模式使用Chrome inspect

    强制开启android webview debug模式使用Chrome inspect https://blog.csdn.net/zhulin2609/article/details/5143782 ...

  4. libcurl使用easy模式阻塞卡死等问题的完美解决---超时设置

    libcurl使用时疑难问题: 在使用libcurl时, jwisp发现, curl_easy_perform是阻塞的方式进行下载的, curl_easy_perform执行后,程序会在这里阻塞等待下 ...

  5. 开启PG的归档模式

    目录 开启PG的归档模式 1.查看pg的数据目录 2.查看pg的归档情况 3.查看归档的模式和位置 4.建立归档目录 5.配置归档参数 6.重启pg 7.查看&&切换归档日志 8.查看 ...

  6. 关于开启url的pathinfo模式

    1.apache要开启pathinfo模式,需要在 <Directory /> Options +Indexes +FollowSymLinks +ExecCGI AllowOverrid ...

  7. vivo 1805的usb调试模式在哪里,开启vivo 1805usb调试模式的流程

    经常我们使用安卓手机通过数据线连接上PC的时候,如果手机没有开启usb调试模式,PC则没办法成功识别我们的手机,部分软件也没办法正常使用,此情况我们需要找方法将手机的usb调试模式打开,下面我们讲解v ...

  8. Android系统中是否开启定位及定位模式的判断

    1.关于Android系统中不同的定位模式 Android系统中包括3中定位模式:   使用GPS.WLAN和移动网络 使用WLAN和移动网络 仅使用GPS 截图 特点 同时使用GPS.WIFI及基站 ...

  9. 如何开启win10的上帝模式

    用了这么久的电脑,小编才知道还有“上帝模式”这一说,原谅小编的孤陋寡闻.翻阅资料才知道,上帝模式简单来说就是一个全能的控制面板,如控制面板的功能.界面个性化.辅助功能选项等方方面面的控制设置,几乎包含 ...

随机推荐

  1. C# mongodb 驱动操作(Z)

    Query.All("name", "a", "b");//通过多个元素来匹配数组 Query.And(Query.EQ("nam ...

  2. 【sublime xftp插件】 Host key verification failed ,错误处理

    错误背景: 1.CentOS7上面作为运行环境,Coding在本机的windows环境 2.在windows上安装sublime 3,然后保存代码通过xftp保存到centos7虚机上面. 3.Cen ...

  3. 进程互斥和fork

    自父进程继承 进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs)) 环境(environment) 堆栈 内存 打开文件的描述符(注意 ...

  4. asp.net mvc 多级文件夹

    ASP.NET MVC - 定制属于你自己的ViewEngine 标签: asp.netmvcstring引擎razorapplication 2012-07-26 14:17 5365人阅读 评论( ...

  5. execl表格VLOOKUP函数的使用

    使用场景:最近在处理一个表格数据的时候出现了一点麻烦.想把另外表2里面对应的的数据放到表1里面,本来表2的ID是乱序的(这里为好看,就顺序排了.),一个个查找复制粘贴比较麻烦,后经大师指点VLOOKU ...

  6. Html 移动web开发细节处理

    1.-webkit-tap-highlight-color:rgba(255,255,255,0)可以同时屏蔽ios和android下点击元素时出现的阴影.备注:transparent的属性值在and ...

  7. 【SSM】Eclipse使用Maven创建Web项目+整合SSM框架

    自己接触ssm框架有一段时间了,从最早的接触新版ITOO项目的(SSM/H+Dobbu zk),再到自己近期来学习到的<淘淘商城>一个ssm框架的电商项目.用过,但是还真的没有自己搭建过, ...

  8. poj 3069 Saruman's Army

    Saruman's Army Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 8477   Accepted: 4317 De ...

  9. docker1.4版本devicemapper修改容器硬盘大小

    升级docker最新版(1.4.0)后,发现容器的硬盘空间只有10G,如果需要指定初始硬盘大小,目前的做法还比较粗暴,更改docker daemon的启动命令,然后删掉目前机器的已有镜像,重启dock ...

  10. 清华学堂 列车调度(Train)

    列车调度(Train) Description Figure 1 shows the structure of a station for train dispatching. Figure 1 In ...