3. 过程
软件开发其他阶段的测试驱动开发,根据测试驱动开发的思想完成对应的测试文档即可。下面针对详细设计和编码阶段进行介绍。
测试驱动开发的基本过程如下:
1) 明确当前要完成的功能。可以记录成一个 TODO 列表。
2) 快速完成针对此功能的测试用例编写。
3) 测试代码编译不通过。
4) 编写对应的功能代码。
5) 测试通过。
6) 对代码进行重构,并保证测试通过。
7) 循环完成所有功能的开发。
为了保证整个测试过程比较快捷、方便,通常可以使用测试框架组织所有的测试用例。一个免费的、优秀的测试框架是 Xunit 系列,几乎所有的语言都有对应的测试框架。我曾经写过一篇文章介绍CppUnit的文章(http://www-900.ibm.com/developerWorks/cn/linux/l-cppunit/index.shtml)。
开发过程中,通常把测试代码和功能代码分开存放,这里提供一个简单的测试框架使用例子,您可以通过它了解测试框架的使用。下面是文件列表。
project/ 项目主目录
project/test 测试项目主目录
project/test/testSeq.cpp 测试seq_t 的测试文件,对其他功能文件的测试文件复制后修改即可
project/test/testSeq.h
project/test/Makefile 测试项目的 Makefile
project/test/main.cpp 测试项目的主文件,不需要修改
project/main.cpp 项目的主文件
project/seq_t.h 功能代码,被测试文件
project/Makefile 项目的 Makefile
主要流程基本如此,但要让你的代码很容易的进行测试,全面又不繁琐的进行测试,还是有很多测试原则和技术需要考虑。
4. 原则
测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。
一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。
测试列表。需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。
测试驱动。这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。
先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。
可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。
及时重构。无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。关于重构,我会另撰文详细分析。
小步前进。软件开发是个复杂性非常高的工作,开发过程中要考虑很多东西,包括代码的正确性、可扩展性、性能等等,很多问题都是因为复杂性太大导致的。极限编程提出了一个非常好的思路就是小步前进。把所有的规模大、复杂性高的工作,分解成小的任务来完成。对于一个类来说,一个功能一个功能的完成,如果太困难就再分解。每个功能的完成就走测试代码-功能代码-测试-重构的循环。通过分解降低整个系统开发的复杂性。这样的效果非常明显。几个小的功能代码完成后,大的功能代码几乎是不用调试就可以通过。一个个类方法的实现,很快就看到整个类很快就完成啦。本来感觉很多特性需要增加,很快就会看到没有几个啦。你甚至会为这个速度感到震惊。(我理解,是大幅度减少调试、出错的时间产生的这种速度感)
5. 测试技术
5.1. 测试范围、粒度
对哪些功能进行测试?会不会太繁琐?什么时候可以停止测试?这些问题比较常见。按大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。感到疲劳就应该停下来休息一下。感觉没有必要更详细的测试,就停止本轮测试。
测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据你的经验,功能复杂、核心功能的代码就应该编写更全面、细致的测试用例,否则测试流程即可。
测试范围没有静态的标准,同时也应该可以随着时间改变。对于开始没有编写足够的测试的功能代码,随着bug的出现,根据bug补齐相关的测试用例即可。
小步前进的原则,要求我们对大的功能块测试时,应该先分拆成更小的功能块进行测试,比如一个类A使用了类B、C,就应该编写到A使用B、C功能的测试代码前,完成对B、C的测试和开发。那么是不是每个小类或者小函数都应该测试哪?我认为没有必要。你应该运用你的经验,对那些可能出问题的地方重点测试,感觉不可能出问题的地方就等它真正出问题的时候再补测试吧。
5.2. 怎么编写测试用例
测试用例的编写就用上了传统的测试技术。
操作过程尽量模拟正常使用的过程。
全面的测试用例应该尽量做到分支覆盖,核心代码尽量做到路径覆盖。
测试数据尽量包括:真实数据、边界数据。
测试语句和测试数据应该尽量简单,容易理解。
为了避免对其他代码过多的依赖,可以实现简单的桩函数或桩类(Mock Object)。
如果内部状态非常复杂或者应该判断流程而不是状态,可以通过记录日志字符串的方式进行验证。
6. Tips
很多朋友有疑问,“测试代码的正确性如何保障?是写测试代码还是写测试文档?”这样是不是会陷入“鸡生蛋,蛋生鸡”的循环。其实是不会的。通常测试代码通常是非常简单的,通常围绕着某个情况的正确性判断的几个语句,如果太复杂,就应该继续分解啦。而传统的开发过程通常强调测试文档。但随着开发节奏的加快,用户需求的不断变化,维护高层(需求、概要设计)的测试文档可以,更低层的测试文档的成本的确太大了。而且可实时验证功能正确性的测试代码就是对代码最好的文档。
软件开发过程中,除了遵守上面提到的测试驱动开发的几个原则外,一个需要注意的问题就是,谨防过度设计。编写功能代码时应该关注于完成当前功能点,通过测试,使用最简单、直接的方式来编码。过多的考虑后期的扩展,其他功能的添加,无疑增加了过多的复杂性,容易产生问题。应该等到要添加这些特性时在进行详细的测试驱动开发。到时候,有整套测试用例做基础,通过不断重构很容易添加相关特性。