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)
    1. {
    1. vector result = {};
    1. ASSERT_EQ(result, getAnagrams(""));
    1. }
  • 变形:应用最高优先级的变形手段返回一个空的结果集合 ({} →nil)
    1. vector getAnagrams(string str)
    1. {
    1. return vector();
    1. }
  • 重构:定义结果集合的数据类型Result,更好的揭示意图。
    1. typedef vector<string> Result;
    1. Result getAnagrams(string str)
    1. {
    1. return Result();
    1. }

测试用例同步重构

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

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

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

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

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

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

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

    1. Result getAnagrams(string str)
    1. {
    1. if (str.size() == 0)
    1. return Result();
    1. else if(str.size() == 1)
    1. return Result{str};
    1. else if(str.size() == 2){
    1. return Result{str,
    1. str.substr(1, 1) + str.substr(0, 1)};
    1. }
    1. }

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

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

呵呵,有点骗人?

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

    1. else{
    1. return Result{string("a") + "bc",
    1. string("a") + "cb",
    1. "bac",
    1. "bca",
    1. "cab",
    1. "cba"};
    1. }

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

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

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

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

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

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

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

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

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

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

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

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

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

    1. string strDelChar(string s, size_t pos){
    1. if (pos == 0)
    1. return "bc";
    1. if (pos == 1)
    1. return "ac";
    1. return "ab";
    1. }

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

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

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

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

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

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

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

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

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

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

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

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

    1. else{
    1. Result res;
    1. for (size_t i = 0; i < str.size(); i++){
    1. res.push_back(strGetChar(str, i) + getAnagrams(strDelChar(str , i))[0]);
    1. res.push_back(strGetChar(str, i) + getAnagrams(strDelChar(str , i))[1]);
    1. }
    1. return res;
    1. }

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

    1. else{
    1. Result res;
    1. for (size_t i = 0; i < str.size(); i++){
    1. Result subRes = getAnagrams(strDelChar(str, i));
    1. for (auto subResStr : subRes){
    1. res.push_back(strGetChar(str, i) + subResStr);
    1. }
    1. }
    1. return res;
    1. }

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

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

    1. Result getAnagrams(string str)
    1. {
    1. if (str.size() == 0)
    1. return Result();
    1. else if(str.size() == 1)
    1. return Result{str};
    1. else if(str.size() == 2){
    1. return Result{str.substr(0, 1) + str.substr(1, 1),
    1. str.substr(1, 1) + str.substr(0, 1)};
    1. }
    1. else{
    1. Result res;
    1. for (size_t i = 0; i < str.size(); i++){
    1. Result subRes = getAnagrams(strDelChar(str, i));
    1. for (auto subResStr : subRes)
    1. res.push_back(strGetChar(str, i) + subResStr);
    1. }
    1. return res;
    1. }
    1. }

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

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

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

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

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

    1. string strDelChar(string s, size_t pos){
    1. return s.substr(0, pos) + s.substr(pos + 1);
    1. }
    1. Result getAnagrams(string str)
    1. {
    1. Result res;
    1. if(str.size() == 1)
    1. return Result{str};
    1. for (size_t i = 0; i < str.size(); i++){
    1. Result subRes = getAnagrams(strDelChar(str, i));
    1. for (auto subResStr : subRes)
    1. res.push_back(str.substr(i, 1) + subResStr);
    1. }
    1. return res;
    1. }

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

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

总结

通过实际的代码操练,体会了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. Excel 相对引用与绝对引用

      相对引用与绝对引用 相对引用与绝对引用的区别在于,当将公式复制到其它单元格时,公式中单元格或单元格区域的地址是否有变化. 相对引用在复制公式时地址跟着发生变化,而绝对引用不会发生变化!绝对引用的方 ...

  2. spring 的IoC的个人理解

    1.ioc IoC的概念介绍 ( a).依赖注入, 分为依赖 和 注入  , 其实依赖是一种耦合方式, 通过spirng在运行时将这种依赖关系完成, 达到解耦目的, 同时达到代码重用, 方便测试,更加 ...

  3. Java中简单的操作(if语句、常用操作符、switch语句、变量赋值等)

    ---------------------if语句介绍--------------------------------------------------- class IfDemo { public ...

  4. AgileEAS.NET SOA 中间件平台.Net Socket通信框架-完整应用例子-在线聊天室系统-下载配置

    一.AgileEAS.NET SOA中间件Socket/Tcp框架介绍 在文章AgileEAS.NET SOA 中间件平台Socket/Tcp通信框架介绍一文之中我们对AgileEAS.NET SOA ...

  5. Android_AsyncTask异步任务(一)

    AsyncTask,是android提供的轻量级的异步类,可以直接继承AsyncTask,在类中实现异步操作,并提供接口反馈当前异步执行的程度(可以通过接口实现UI进度更新),最后反馈执行的结果给UI ...

  6. 【转】Java内存管理:深入Java内存区域

    转自:http://www.cnblogs.com/gw811/archive/2012/10/18/2730117.html 本文引用自:深入理解Java虚拟机的第2章内容 Java与C++之间有一 ...

  7. BZOJ 1076 & 撞鸭递推

    题意: 还是看原题题面好... 你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关.在这个奖励关里,系统将依次随 机抛出k次宝物,每次你都可以选择吃或者不吃(必须在抛出下一个宝物之前做出选择,且现在决 ...

  8. Theano:LSTM源码解析

    最难读的Theano代码 这份LSTM代码的作者,感觉和前面Tutorial代码作者不是同一个人.对于Theano.Python的手法使用得非常娴熟. 尤其是在两重并行设计上: ①LSTM各个门之间并 ...

  9. poj1236Network of Schools Tarjan裸题

    其实就是手打了个Tarjan的模板 输出的时候注意是入度为0的点的个数和max(入度0的个数,出度0的个数),在n=1时特判为0即可 ——以后图论要渐渐模板化,方便使用 #include <cs ...

  10. Graphviz从入门到不精通

    1.安装Graphviz (windows 版本,后面说linux下的安装) 1.1)下载安装文件 从graphviz官网下载 http://www.graphviz.org/Download.php ...