“目前,软件测试已经不再是破坏性的了,而是作为一种建设性的实践贯穿整个软件的生命周期。软件的开发过程,不必像传统那样,先编码,再测试,通过软件测试来发现软件中潜在的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 初稿完成。