Test-Driven Development:一次的成功的TDD实践

  “目前,软件测试已经不再是破坏性的了,而是作为一种建设性的实践贯穿整个软件的生命周期。软件的开发过程,不必像传统那样,先编码,再测试,通过软件测试来发现软件中潜在的bug,而可能采用一种测试先行的策略来构建高效,可靠的软件代码。Test-Driven Development,即TDD,就是这样一种建设性的测试促进开发的实例。”

  上面的一段话,是对当前软件开发现状,方式精髓的概括(自己口头总结)。Test-Driven Development,由大师Kent Beck经典著作Test-Driven Development by example引入读者视野已经有很长的一段时间。TDD的思考方式,开发步骤(编写一个测试,运行所有的测试并失败,做微小的修改尽快使测试代码成功运行,运行所有的测试程序并全部通过,重构代码以消除重复和优化设计结构),仅仅五步,很简单,但是在实际的项目中又很难按部就班地操作。

  软件工程的老师在谈到Test-Driven Development时,也强调虽然TDD能够帮助写出高质量的代码,加快开发进度,但是TDD入门很困难。TDD入门虽难,但作为志存高远的程序员总不能因噎废食吧。我花了好长的时间,研究Kent Beck的神作,看了好多示例,回忆课堂老师所教,最终成功地进行了一次全新的TDD实践。

  实践的题目是从ACM之家上随便选的,因为这样更贴合实际的开发需求,但是为了简单点,选择了数值类的题目(详细的链接:http://www.acmerblog.com/max-sum-rectangle-in-a-matrix-5955.html)。

  下面具体谈谈自己的实践过程。

  题目:输入一个整型数组,数组里有正数也有负数,数组中一个或连续的多个整数组成一个子数组,求所有子数组的和的最大值。如数组{1,2,3},可能的子数组为{1},{2},{3},{1,2},{2,3},{1,2,3},子数组和是1,2,3,3,5,6,最大值是6。

  拿到题目,什么都不需要考虑,先建立JUnit Test Case,即SubArrayTest类。写出最简单的一个测试,即数组为{0}的测试,子数组和的最大值为0,测试代码如下:  

 1 package org.warnier.zhang.demo.test;
 2 
 3 import static org.junit.Assert.assertEquals;
 4 
 5 import org.junit.Before;
 6 import org.junit.Test;
 7 import org.warnier.zhang.demo.SubArray;
 8 
 9 public class SubArrayTestTest {
10 
11     SubArray subArray;
12     @Before
13     public void setUp() throws Exception {
14         subArray = new SubArray();
15     }
16 
17     @Test
18     public void test_array_0_maxsum_0(){
19         int[] array = {0};
20         subArray.setArray(array);
21         int expected = 0;
22         int actual = subArray.getMaxSum();
23         assertEquals(expected, actual);
24     }
25 }

  “有人说TDD没有设计?”这是一个伪命题!TDD也是需要设计的,从上面的代码中就可以看出,我们假设SubArray类有setArray()方法,接受一个数组,getMaxSum()方法,返回子数组和的最大值。

  自动修改(Ctrl + 1)的SubArray类的代码如下: 

 1 package org.warnier.zhang.demo.test;
 2 
 3 public class SubArray {
 4     int[] array;
 5     
 6     public void setArray(int[] array) {
 7         this.array = array;
 8     }
 9 
10     public int getMaxSum() {
11         return 0;
12     }
13 
14 }

  点击运行按钮,发现指示条绿了,一个测试搞定。

  添加一个测试,数组{1},子数组和的最大值为1,测试代码如下: 

1     @Test
2     public void test_array_1_maxsum_1(){
3         int[] array = {1};
4         subArray.setArray(array);
5         int expected = 1;
6         int actual = subArray.getMaxSum();
7         assertEquals(expected, actual);
8     }

  运行失败,修改,重构SubArray的代码如下:

1   public int getMaxSum() {
2         max_sum = array[0];
3         return max_sum;
4     }

  点击运行按钮,测试通过。容易知道,当数组{}只包含一个整数的情况均满足。

  紧接着写一个新的测试,数组{-1,2},子数组和的最大值是2,测试代码如下:

1   @Test
2     public void test_array_f1_2_maxsum_2(){
3         int[] array = {-1, 2};
4         subArray.setArray(array);
5         int expected = 2;
6         int actual = subArray.getMaxSum();
7         assertEquals(expected, actual);
8     }

  运行失败,修改SubArray的代码如下:

 1 package org.warnier.zhang.demo.test;
 2 
 3 public class SubArray {
 4     int[] array;
 5     int max_sum;
 6     int n_sum = 0;
 7     
 8     public void setArray(int[] array) {
 9         this.array = array;
10     }
11 
12     public int getMaxSum() {
13         max_sum = array[0];
14         for(int i = 0; i < array.length; i++){
15             if(max_sum < array[i]){
16                 max_sum = array[i];
17             }
18             n_sum += array[i];
19         }
20         return max_sum > n_sum ? max_sum : n_sum;
21     }
22 
23 }

   算法思想是先找出单个元素子数组的最大值,再与两元素子数组和进行比较取最大值。容易知道数组{}中包含两个整数的情况均满足。

  新增一个测试,数组{-1,2,3},子数组和的最大值是5,测试代码如下:

1   @Test
2     public void test_array_f1_2_3maxsum_5(){
3         int[] array = {-1, 2, 3};
4         subArray.setArray(array);
5         int expected = 5;
6         int actual = subArray.getMaxSum();
7         assertEquals(expected, actual);
8     }

  运行失败,修改SubArray的代码如下:

 1   public int getMaxSum() {
 2         max_sum = array[0];
 3         int t_sum = 0;
 4         for(int i = 0; i < array.length; i++){
 5             if(array.length != 1 && i != array.length - 1){  6                 for(int j = i; j < i + 2; j++){  7                     t_sum += array[j];  8  }  9                 if(max_sum < t_sum){ 10                     max_sum = t_sum; 11  } 12                 t_sum = 0; 13  } 14             if(max_sum < array[i]){
15                 max_sum = array[i];
16             }
17             n_sum += array[i];
18         }
19         return max_sum > n_sum ? max_sum : n_sum;
20     }

    算法思想是通过一个步长为2的子循环(即淡蓝色标记的代码块),来比较子数组{-1,2},{2,3}和的最大值;  

  点击运行,测试通过。

  到此为止,没必要没头苍蝇似的继续写测试用例了,上面的代码很容易看出逻辑有些混乱,是时候按照TDD的开发步骤,重构一下代码了。脑袋中不禁冒出几个问题:

  (1)代码的逻辑是否有点乱?

  (2)能否将数组{}元素只有一个整数的情况整合到上面步长为2的循环中?

  (3)能否将所有整数的组成的子数组的情况整合到上面步长为2的循环中?

  (4)能否消减 n_sum 和 t_sum 局部变量为一个?

  (5)上面代码中红色标记的 2 有没有什么特殊的含义?

  思考着上面的5个问题,对已经编写的代码重新进行重构,重构后的代码如下:

 1   public int getMaxSum() {
 2         int max_sum = array[0];
 3         int n_sum = 0;
 4         for(int i = 0; i < array.length; i++){
 5                 int flag = 1;
 6                 while(flag <= array.length){
 7                     if((i + flag) <= array.length) {
 8                         for(int j = i; j < i + flag; j++){
 9                             n_sum += array[j];
10                         }
11                         if(max_sum < n_sum){
12                             max_sum = n_sum;
13                         }
14                         n_sum = 0;
15                     }
16                     flag ++;
17                 }
18         }
19         return max_sum;
20     }

   运行现有的测试代码,熟悉的绿色条又出现了。

  现在新增一个测试,数组{-1,2,3,4},子数组和的最大值是9,测试代码如下:

1   @Test
2     public void test_array_f1_2_3_4maxsum_9(){
3         int[] array = {-1, 2, 3, 4};
4         subArray.setArray(array);
5         int expected = 9;
6         int actual = subArray.getMaxSum();
7         assertEquals(expected, actual);
8     }

    运行测试,顺利通过。未对代码做任何修改!

  Test-Driven Development进行到此是不是就可以结束了?到了这里,就仁者见仁,智者见智了。如果有的人还不放心,那么还可以接着再写几个测试用例。但是,根据软件测试的原则知道,“穷举测试是做不到的”,新增几个测试无法从根本上增加自己对代码的信心。我的做法是,使用覆盖率计算工具,来计算测试代码的覆盖率。推荐使用EclEmma,下面是上面TDD代码的覆盖率截图:

    都是100%,不必惊讶。其实,用Test-Driven Development写的代码,覆盖率都是杠杠的!

  到此为止,我的一次成功的Test-Driven Development实践就结束了。也许,读者会说,我的TDD实践,貌似挺顺利的啊,新增测试,修改代码去bug,重构,都一步一步顺理成章地做了下来。事实上,我在实践的过程中也遇到了很多的坎,只是在写这篇总结博客时,去掉了当时的思考过程,因此显的清晰了很多。TDD很大的程度上,是一种在总结代码走向的基础上修改设计,消除重复的过程。

  虽然 Test-Driven Development诞生有些年头了,但是不论是市售教材,网上只会讲“加法”示例的视频教程,课堂上模模糊糊的讲解,都无法与自己产生共鸣。自己的这次实践,可能稍显简单,希望给那些对TDD感兴趣的读者一点启迪。

  作为即将毕业的本科生,水平有限,上面的代码可能写错的地方,欢迎大家指出,回复交流,或者通过邮箱联系我([email protected])。

  参考文献:

    [1] Test-Driven Development by example,Kent Beck著,白云鹏译,2013.7,北京,机械工业出版社。 (看英文吧,翻译真的很烂。) 

    [2] 验收测试驱动开发 ATTD实例详解,Markus Gartner著,张绍鹏,冯上译,2013.5,北京,人民邮电出版社。

    [3] The Art of Software Testing,Glenford J. Myers著,张晓明,黄琳译,2012.3,北京,机械工业出版社。

  本文历史:

  • 2015-05-02  初稿完成。

 

 

你可能感兴趣的:(Test-Driven Development:一次的成功的TDD实践)