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  初稿完成。
时间: 2024-10-12 17:11:24

Test-Driven Development:一次的成功的TDD实践的相关文章

Domain Driven Development相关概念

Entity 与 Value Object1,Entity有唯一的身份标识,是可变的对象.Value Object是immutable,创建了就不能改变.2,Value Object可以在多个领域之间重用,Entity是对实际领域的抽象.3,Entity包含完整信息,Value Object只包含部分信息 怎么识别聚合对象聚合对象是一组关系非常近的Entity或value Object的集合,通过聚合在这些对象周围开成固定的边界.聚合根是外部引用的入口. Evans关于聚合的两条推荐准则: 1)

phpunit测试成功 phpunit测试实践代码

16:12 2015/12/8phpunit测试成功,代码写在www目录下,以类名命名代码文件,我的文件名为 ArrayTest.php,类名为ArrayTest,内部写了简单的测试代码:<?php// require_once 'PHPUnit/Autoload.php';// require_once 'ArrayTeller.class.php';// require_once 'PHPUnit/Framework.php'; class ArrayTest extends PHPUnit

测试驱动开发(TDD)

在编写程序之前,先确定程序中的变量.控件等元素允许的值.若在编写程序时,变量.控件中的值与事先确定的值不相符,就说明程序的某处有bug,这种测试方法就是TDD(Test Driven Development,测试驱动开发).TDD与OpenGL ES一样,只是一套标准或一套API.Android SDK中提供了一套测试框架(JUnit),可用于对Android应用程序进行TDD测试.测试框架的特性如下: Android的测试框架基于JUnit.可在无需调用Android SDK API的情况下测

Domain Driven Design and Development In Practice--转载

原文地址:http://www.infoq.com/articles/ddd-in-practice Background Domain Driven Design (DDD) is about mapping business domain concepts into software artifacts. Most of the writings and articles on this topic have been based on Eric Evans' book "Domain Dr

测试驱动开发实践 - Test-Driven Development

一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行类或者用户界面的开发.由于要先开发测试用例,那么开发人员就必须清楚测试的目的,所测功能模块的业务逻辑以及需要测试的场景. 这样TDD确保了项目的代码与所需的业务是匹配的,并且在日后的开发工作中也能确保之前所做的功能的可测试性. 很多同学问TDD是使用那种编程语言,或者是某种技术,这里需要明确的是,T

测试驱动开发实践 - Test-Driven Development(转)

一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行类或者用户界面的开发.由于要先开发测试用例,那么开发人员就必须清楚测试的目的,所测功能模块的业务逻辑以及需要测试的场景. 这样TDD确保了项目的代码与所需的业务是匹配的,并且在日后的开发工作中也能确保之前所做的功能的可测试性. 很多同学问TDD是使用那种编程语言,或者是某种技术,这里需要明确的是,T

自己总结的 iOS ,Mac 开源项目以及库,知识点------持续更新

自己在 git  上看到一个非常好的总结的东西,但是呢, fork  了几次,就是 fork  不到我的 git 上,干脆复制进去,但是,也是认真去每一个每一个去认真看了,并且也是补充了一些,感觉非常棒,所以好东西要分享,为啥用 CN 博客,有个好处,可以随时修改,可以持续更新,不用每次都要再发表,感觉这样棒棒的 我们 自己总结的iOS.mac开源项目及库,持续更新.... github排名 https://github.com/trending,github搜索:https://github.

iOS开发 非常全的三方库、插件、大牛博客等等

UI 下拉刷新 EGOTableViewPullRefresh- 最早的下拉刷新控件. SVPullToRefresh- 下拉刷新控件. MJRefresh- 仅需一行代码就可以为UITableView或者CollectionView加上下拉刷新或者上拉刷新功能.可以自定义上下拉刷新的文字说明.具体使用看"使用方法". (国人写) XHRefreshControl- XHRefreshControl 是一款高扩展性.低耦合度的下拉刷新.上提加载更多的组件.(国人写) CBStoreHo

测试框架mochajs详解

测试框架mochajs详解 章节目录 关于单元测试的想法 mocha单元测试框架简介 安装mocha 一个简单的例子 mocha支持的断言模块 同步代码测试 异步代码测试 promise代码测试 不建议使用箭头函数 钩子函数 钩子函数的描述参数 异步的钩子函数 全局钩子 延迟启动测试 测试用例TODO 仅执行一个用例集/用例 跳过哪些用例集/用例 重新执行用例 动态生成用例 测试时间 测试超时 用例集执行超时 用例执行超时 钩子函数超时 diff差异比较功能 mocha使用命令和参数 mocha