基于Java实现TDD测试驱动开发
所谓TDD,是指测试驱动开发,英文全称为:Test-Driven Development。从字面意思,不难理解,就是利用基于测试用例驱动的一种开发过程,通俗一点来说,就是我们在设计一段程序和一个功能之前,先思考和设计测试用例,进而反射促进代码的开发。那么什么时候代码的开发工作算是完成了呢?那就是,只要所有的事先设计的测试用例都执行通过了,则代码就算完成开发,完成验收。开发人员就可以继续下一个功能的测试和开发。
实验简介
所谓TDD,是指测试驱动开发,英文全称为:Test-Driven Development。从字面意思,不难理解,就是利用基于测试用例驱动的一种开发过程,通俗一点来说,就是我们在设计一段程序和一个功能之前,先思考和设计测试用例,进而反射促进代码的开发。那么什么时候代码的开发工作算是完成了呢?那就是,只要所有的事先设计的测试用例都执行通过了,则代码就算完成开发,完成验收。开发人员就可以继续下一个功能的测试和开发。
本节将基于ArrayCompare程序中的StringHandle类中的isNumber()方法进行TDD实践。
实验目的
(1) 掌握TDD测试驱动开发的实践过程。
(2) 理解测试技术在软件研发过程中的重要价值。
实验流程
1. TDD实施过程
2. TDD遵循的基本原则
(1) 独立测试:不同代码的测试应该相互独立,一个类对应一个测试类(对于C代码或C++全局函数,则一个文件对应一个测试文件),一个函数对应一个测试函数。用例也应各自独立,每个用例不能使用其他用例的结果数据,结果也不能依赖于用例执行顺序。 一个角色:开发过程包含多种工作,如:编写测试代码、编写产品代码、代码重构等。做不同的工作时,应专注于当前的角色,不要过多考虑其他方面的细节。
(2) 测试列表:代码的功能点可能很多,并且需求可能是陆续出现的,任何阶段想添加功能时,应把相关功能点加到测试列表中,然后才能继续手头工作,避免疏漏。
(3) 测试驱动:即利用测试来驱动开发,是TDD的核心。要实现某个功能,要编写某个类或某个函数,应首先编写测试代码,明确这个类、这个函数如何使用,如何测试,然后在对其进行设计、编码。
(4) 先写断言:编写测试代码时,应该首先编写判断代码功能的断言语句,然后编写必要的辅助语句。
(5) 可测试性:产品代码设计、开发时的应尽可能提高可测试性。每个代码单元的功能应该比较单纯,“各家自扫门前雪”,每个类、每个函数应该只做它该做的事,不要弄成大杂烩。尤其是增加新功能时,不要为了图一时之便,随便在原有代码中添加功能。
(6) 及时重构:对结构不合理,重复等“味道”不好的代码,在测试通过后,应及时进行重构。
(7) 小步前进:软件开发是复杂性非常高的工作,小步前进是降低复杂性的好办法。
3. 梳理isNumber()方法的需求点
根据功能要求,我们可以列举如下一些规则,进而更清晰,更全面覆盖各种可能的输入:
(1) 有效的数字必须只能包含0-9的数字,小数点和负号。
(2) 如果有小数点和负号,小数点最多只能有一个,负号最多只能有一个。
(3) 如果是负数,则负号必须在第一位。
(4) 如果是负小数,小数点必须在第二位之后。
(5) 如果不是小数,则不能以0开头。如果是负整数,则第二位不能为0.
4. 设计测试用例
根据上术对功能需求和检查规则的整理,我们可以设计如下一些测试用例:
(1) 有效的值:"12345", "-12345", "123.45", "-123.45", "0.123", "-0.12345", "-.123", "123.", ".12345"。
(2) 无效的值:"012345", "1-23", "0.-123", "12345-", "123T", "00000", "-1.23-", "123.45.56", "123*5","-0123", "12中3 ","12#3 "。
上述的有效值和无效值便可以作为我们的测试用例用于对被测试方法进行测试。
5. 让isNumber()方法可运行
最开始,我们只需要设计一个简单的isNumber(),有参数,有返回值,无任何处理逻辑,仅仅只是简单的保证代码可以运行,代码如下:
// 设计实现代码isNumber方法 public boolean isNumber(String source) { return true; } |
6. 开发测试驱动程序
package com.woniuxy.test; public class TDDDemo { public static void main(String[] args) { TDDDemo tdd = new TDDDemo(); tdd.testIsNumber("12345", true); tdd.testIsNumber("-12345", true); tdd.testIsNumber("123.45", true); tdd.testIsNumber("-123.45", true); tdd.testIsNumber("0.123", true); tdd.testIsNumber("-0.12345", true); tdd.testIsNumber("-.123", true); tdd.testIsNumber("123.", true); tdd.testIsNumber(".12345", true);
tdd.testIsNumber("012345", false); tdd.testIsNumber("12-3", false); tdd.testIsNumber("0.-123", false); tdd.testIsNumber("12345-", false); tdd.testIsNumber("123T", false); tdd.testIsNumber("00000", false); tdd.testIsNumber("-1.23-", false); tdd.testIsNumber("123.45.56", false); tdd.testIsNumber("123*5", false); tdd.testIsNumber("-0123", false); tdd.testIsNumber("12中3", false); }
// 测试接口isNumber()的测试驱动程序 public void testIsNumber(String source, boolean expect) { boolean actual = this.isNumber(source); if (actual == expect) { System.out.println("StringHandle-isNumber: 测试成功."); } else { System.out.println("StringHandle-splitString: 测试失败: " + source); } }
// 设计实现代码isNumber方法 public boolean isNumber(String source) { return true; } } |
7. 进一步完善isNumber()方法
毫无疑问,上述测试代码执行时会出错,也没有办法通过测试。因为我们还没有真正实现其功能。所以接下来,我们就得完善isNumber()的代码,真正开始开发代码:
// 设计实现代码isNumber方法 public boolean isNumber(String source) { int minus = 0; // 统计负号的个数 int point = 0; // 统计小数点的个数 for(int i=0; i<source.length(); i++){ int code = source.charAt(i); // 获取每个字符的ASCII码 // 如果某个字符不属于0-9或负号或小数点,则可以判定为无效数字 if (code<45 || code > 57 || code == 47) return false; // 统计字符串中负号和小数点的个数 if(code == 45) minus++; if(code == 46) point++; } // 如果字符串中的负号或小数点超过1个,则无效 if(minus > 1 || point > 1) return false; // 如果有负号,但是负号不在第一位,则无效 if(minus == 1 && source.charAt(0) != 45) return false; // 如果以上情况都未出现,则说明是正确的数字 return true; } |
8. 继续运行测试
上述代码直接从StringHandle这个类当中复制过来,由于我们没有考虑到以0开头的整数这一情况,所以最终运行的结果为:
StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-splitString: 测试失败: 012345 StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-splitString: 测试失败: 00000 StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-isNumber: 测试成功. StringHandle-splitString: 测试失败: -0123 StringHandle-isNumber: 测试成功. |
9. 再一步完善代码
基于上述测试结果,我们进一步完善代码,把以0开头的整数这种情况对应的代码逻辑完成,即可通过所有测试,至此,代码开发完成。(此处为了节省篇幅,不再演示代码,请读者朋友自行完成,并思考这个过程)。
10. 关于TDD的扩展思考
TDD听起来很美,但是在实际执行过程中,我们发现很多团队是很难将TDD实施下去的。除了我们的研发纪律和管理水平之外,更重要的一点是,我们许多研发人员觉得与其有那个时间去写测试驱动程序,思考测试用例,还不如多花点时间扣代码来得实在。当然,根据笔者多年的经验观察,这是一种非常短视的行为。这有几个方面的原因,简单分析如下:
(1) 没有测试驱动程序,每一次对代码的新增或者修改,开发人员必然要进行适当的调试,而调试是不具备重复价值的,是一次性的工作。而代码级测试程序是可以重复使用的,任何时候对代码进行的修改,都可以马上运行进而几乎瞬间地得到结果。所以,虽然,我们可能节省了1个小时去思考和完善测试用例,但是我们却会花2个小时甚至更多时间去进行代码的多次调试。
(2) 由于为了节省时间,直接上手写代码,导致对客户的很多需求思考不全面。从初期开发来看,问题根本不会暴露,但是一旦到了后期,我们才发现需求有问题,或者团队之间对需求的理解不一致,比如测试人员和开发人员的理解不一致,导致我们需要更多的时间讨论,确认,甚至返工。
(3) 如果前期对代码进行了全面的测试设计和执行,并且是自动化来进行处理的。那么每一个版本的发布,我们都可以执行该测试代码,对所有方法或接口的功能进行测试。则我们可以免去大量的系统测试工作量,进而将系统测试转而重点关注用户体验,关注兼容性和可靠性等测试工作。而不用关注一些简单的功能层面的问题。将节省大量的人才物力。同时,在回归测试上,也将节省大量人力物力,因为我们不用再去操心修改代码或新增功能带来的对老功能的影响。想想就是令人激动的。
(4) 通过引入TDD开发方法,可以倒逼研发团队考虑代码的可测试性,设计结构更加良好,可重用性更高,代码块功能更独立,可维护性更强的代码。使整个研发团队更加专业,对开发工作有更加深入的理解。
(5) 由于前期已经对需求和代码验收标准进行了深入思考,所以在开发代码或对代码进行变更时将会非常有信心。同时,能够节约更多的时间。
那么,为什么看上去全是优点的一种开发基础方法论,却得不到大多数研发团队的接纳和认可呢?通过笔者与国内一些研发人员和技术主管的沟通交流,得出了如下一些原因。
(1) 项目紧,任务重,没时间。虽然团队认可TDD以及敏捷开发等方法论,但是大家每天忙于项目,忙于代码实现,忙于解决BUG,实在没有时间来思考测试。
(2) 对质量理解非常肤浅。尤其是程序员团队,几乎没有什么质量意识,甚至很多时间连如何写出一个优雅的代码结构这件事情都没怎么整明白,更别说对软件测试和产品质量有多少意识了。对自己的代码也没有敬畏之心,不负责任。觉得这些事情不就是交给专门的测试工程师就可以了吗?
(3) 不太理解和尊重测试,甚至测试人员。觉得测试是没有什么技术含量的。不瞒各位读者,这种观念在很多研发团队中根深蒂固,觉得那帮搞测试的就是点点鼠标,然后没事找茬,甚至很多研发负责人,如果是程序员出身,都不了解测试。这个时候,我们谈测试驱动开发,从感情上和面子上,是接受不了的。当然,事实上测试有没有技术含量,有多少技术含量,是否值得尊重,笔者相信,读完这本书,你会有明确的态度。
(4) 很多代码,没有办法进行自动化测试。由于代码结构设计得乱七八糟,很多代码就是天生残疾,根本就没有思考过代码的可测试性设计。然而,一个研发团队如果没有铁的纪律,再优秀的个人,都无法保障产品的质量。
(5) 对代码规范的监督执行不到位。每个团队都有规范,但是能否执行到位,基本很难。每个团队都会进行产品的测试,但是,相信有过实际项目经验的读者朋友,应该可以感受到,自己所在团队的测试专业度。笔者见过的绝大多数研发测试团队,非常不专业。
(6) 项目需求的不确定性。国内的研发团队,很多项目的启动都不是建议在完整的需求理解和需求评审的情况下进行的,甚至很多代码都是边做边改,这个时候,再来写过多的TDD代码,实在是浪费时间。就像我们的研发团队,在很多文档工作上的处理一样,只为完成任务。
当然,并不是说不使用TDD的团队一定就有问题,TDD只是众多方法论中的一种,甚至现在也有很多反对TDD的声音,这些根本不是问题的关键,每个人,每个团队,都有选择的权利。笔者更想说的是,研发团队管理,以及研发方法的应用,其实我们已经积累了很多最佳实践,我们随便选择一种方法,只要难免坚守纪律,坚持实施到位,一定会有收获。
关于TDD,笔者在8年前,提出了另外的一种TDD的解释:Test Drives Development,即测试驱动开发,当然,此TDD的主语是Test,不是Development,这与前面所述的TDD,有本质上的产品。也希望通过本书,为更多的人传递这样一种理念,测试技术或者测试团队是完全有能力能够驱动整个产品的研发过程中。每个团队都天天喊口号,质量是第一生命线。而实际上的情况怎么样,相信大家有目共睹,很多项目,包括一些百万级的大项目,根本就没有测试。放在今天,这实在是一件令人难以想像的事情。如果我们能够把产品的研发过程全权交给质量部分来负责推动,倒逼整个研发团队进行全员质量管理,这个时候,我们来谈质量是生命这件事情,应该会更有底气。
思考练习
(1) 请总结TDD的优点与不足?
(2) 请与自己项目组的程序员讨论TDD的价值。
(3) 请调研一下自己或朋友的公司,是否正在应用TDD?为什么用,为什么不用?