自述:论述单元测试的文章已经很多很多了,但是单元测试是否已经真正成为软件开发过程中不可或缺的一个重要活动呢?我不知道其他公司的情况如何,我知道起码在我们公司还不是的,目前我们基本上还处于没有任何单元测试的水平上。而这种情况带来的痛苦实在是让人心惊胆战,我不知道自己写的代码什么时候就会蹦出个Bug来挑战自己的神经。本文是我为了推动单元测试在公司内的普及所写的一篇文章,我对单元测试的认识基本都来自网上,也有部分是我和我的同事们在平常工作中认识到的。本文没有多少创新的观点,贴出来是希望能得到做过单元测试推广工作的朋友们的建议,也希望为那些需要做单元测试推广工作的朋友们提供一点帮助。
1 概述
1.1 单元测试概述
所谓单元测试,是指对软件设计的最小单位,进行正确性检验的测试工作。而检验的方法,一般来说是由开发人员编写一小段测试代码,根据被测目标代码的应用场景,设计拥有合理覆盖度的输入条件,调用执行目标代码,然后判断输出结果是否与预期一致。被测试目标代码一般应具体到类的方法层面上。
总之,单元测试是一个方法层级上的测试,单元测试也是最细粒度的测试,用于测试一个类的每一个方法都已经满足了方法的功能需求。
单元测试的目的在于发现目标代码中可能存在的错误。单元测试一般与编码同步进行,以便及时发现编码过程中可能存在的缺陷并使其尽早得以修正。当然,在开发后期也可以开展单元测试活动,用以保证代码质量,支持后续的代码重构。
在现代软件开发周期中,以拥抱需求变化、倡导快速开发等理念日益获得业界重视的敏捷开发过程十分推崇单元测试,已经把单元测试作为贯穿整个开发周期的一个重要的开发活动。在敏捷开发过程中,有持续集成和渐进提交的方法论,已经总结出了非常好的单元测试理论和实践。如TDD实践的提出,将单元测试提升到与需求分析一致的地位,直接弱化设计阶段,它认为可以通过单元测试活动来驱动整个开发过程。
注:此处参考袁光东在IT168上的文章《程序员为什么不写单元测试? 》
1.2 本文档目的
阐明单元测试的重要意义,定义单元测试的实施过程,依据公司的OSSP规范,对OSSP中与单元测试有关的论述进行补充,积极探索改进软件开发过程的方法,为公司OSSP规范的完善和进化提供有益支持。
2 公司的OSSP规范
2.1 OSSP概述
OSSP是指Organization’s Set of Standard Process,即组织标准软件过程,描述组织中所有项目的软件开发过程中必须满足的一些需求。OSSP的目的是在组织的各项目中建立起公共过程,支持过程的度量、持续性和改进。
公司已经通过CMMI3认证,因此也已经建立起一套适合公司实际情况的OSSP规范,对公司所有项目的研发过程进行了明确划分和定义,并同时提出了对各项活动过程进行裁剪的指导建议。其中涉及单元测试的部分有如下两个过程:
一、测试过程
在测试(Test)过程中定义了单元测试的活动,指出其目的是:对软件各模块进行单元测试,寻找并改正缺陷,保证软件质量;并指出单元测试一般由开发人员完成,制定开发计划时应该考虑包含实施单元测试的测试计划;同时还规定了单元测试活动的输出是《单元测试检查表》。
二、技术解决过程
该过程共分成五个子过程,“编码与单元测试”是最后一个子过程,其中明确要求“对每个单元、关键类、关键算法、关键业务进行单元测试”。
下面我们来详细分析一下这两个过程对单元测试的论述。
2.2 测试过程
2.2.1 测试过程对单元测试活动的论述
目的(Purpose)
对软件各模块进行单元测试,寻找并改正缺陷,保证产品质量。单元测试一般由开发人员来完成。
角色和职责(Roles and Responsibility)
角色 | 职责 |
项目经理 | 制定单元测试计划。 |
开发人员 | 编写测试用例,执行测试,修改错误。 |
进入条件(Entry Criteria)
- 按测试计划的安排,项目进行到单元测试阶段。
- 程序可进行测试。
活动流程图(Activity Flow Diagram)
任务描述(Task Description)
一、 制定单元测试的测试计划,可以包含在项目开发计划中;
二、 开发人员首先撰写单元测试检查表(测试用例)。
三、 开发人员根据“单元测试计划”和相应的“单元测试检查表”来测试同伴或自己的代码;
四、 记录测试结果,分析测试结果,对Bug进行纠正并记录。
五、 在单元测试结束时做成“单元测试检查报告”。
输出(Outputs)
- 《单元测试检查表》
2.2.2 测试过程对单元测试活动论述的不足之处
- 进入条件描述比较模糊
理论上,编写单元测试代码应该与编写被测目标代码同步进行,甚至可以先于目标代码编写。笼统地说“按测试计划的安排,项目进行到单元测试阶段”缺少具体的指导意义。
当然,单元测试也可以在目标代码全部完成之后进行,这主要用于监控目标代码的变动。
- 该活动更适合对单元模块进行的功能测试
从活动流程图、任务描述和输出的单元测试检查表中可以发现,此处单元测试更多地遵循功能测试的思路,而不是从代码测试角度来制定本过程。
2.3 技术解决过程
2.3.1 技术解决过程与单元测试有关的论述
过程总体概述(Process Overview)
编码与测试活动流程图(Activity Flow Diagram)
编码与测试活动的任务描述摘录
编码与单元测试一般要经历“编程—>代码审查—>单元测试—>模块测试”等步骤,并且通常需要反复迭代,“缺陷管理与改错”贯穿始终。
(1) 编程
- 开发人员根据“编程计划”编写软件的代码,并随时记录编程技术、问题与对策、心得体会等等,产生《编程文档》(类似于编程日记)。
- 开发人员在编写完成每个模块时,必须对自己的代码进行必要的审查和测试。
(2) 代码审查
- 由品质保证部对代码按照相关代码规范进行审查,并填写《代码审查报告》。
(3) 单元测试
- 开发人员首先撰写单元测试用例。
- 开发人员根据“单元测试计划”和相应的“测试用例”来测试同伴的代码,产生“测试报告”。
2.3.2 技术解决过程对单元测试活动论述的不足之处
从编码与测试流程图及其任务描述中可以看出,技术解决过程对单元测试的时机、内容、结果等方面都做了规定。但是应该认识到,单元测试不是一个单薄的活动,单元测试是代码级的白盒测试,测试方法与软件代码和软件运行环境密切相关,对软件实施单元测试前需要对软件代码和运行环境进行分析,确定相应的单元测试方法。
技术解决过程没有明确规定进行单元测试的详细方法和过程,也没有制定严格的单元测试审查标准。开发人员在实际开发过程中,由于缺乏实施单元测试的方法指导,很难对工作代码进行有效的单元测试,也就看不到单元测试对开发活动所能产生的积极效用,这些原因导致单元测试活动经常被忽略,使我们不得不依赖紧密地依赖测试人员的功能测试工作,以确定我们的代码能够正确运行。但是,功能测试(包括模块测试、集成测试、回归测试、系统测试等)的效果严重依赖于测试人员的工作,不在我们能够掌控的范畴之内。
如果我们写的代码没有经过严格和有效的单元测试,那么它们的质量或多或少将会存在隐患,我们将带有质量隐患的代码发布出去,这些隐患可能被测试人员发现,也可能直到生产环境中才暴露出来,可想而知,这时我们的工作将会极为被动。
如果我们实行严格有效的单元测试,那么可以在很大程度上确保代码的质量,并为后续的改进、重构、维护等工作提供切实的保障。
3 单元测试的必要性
3.1 不做单元测试的后果
项目进度的黑洞
- 完成99%(最后的1%永远无法完成)
- 2个月完成开发,半年过去了还是不稳定
- 骨干员工都投入在维护以前项目上
- 没有力量进行新的研发
- 越来越难维护,导致产品开发难以创新
研发成本的大量消耗
骨干员工才能进行维护
- 由于模块间耦合,只有骨干才了解整体情况
- 骨干每天都在Debug(没有力量进行新的研发)
问题的解决周期(8小时)
- 定位问题4~6小时
- 解决问题1~2小时
- 浪费了(4/5=80%左右的维护工作量)
软件质量故障延迟解决的代价
- 开发人员提交了带有隐患的代码
- 测试人员可能轻易发现故障、可能很难发现故障
- 发布到生产环境中的故障被用户使用时发现
- 我们的软件运行不稳定
难以为继
- 为了修正问题的改动缺少整体考虑
- 可维护性不断下降
- 不同维护人员思路不同(时间长了容易出现冲突)
- 长期的结果是导致代码必须重写
3.2 单元测试的优点
是一种验证行为
- 不会出现项目进度的黑洞
- 完成的功能都得到了验证
- 不会出现产品不稳定的情况
- 不用担心修改导致程序结构被破坏
- 任何人都可以维护项目
- 不用担心修改导致程序结构被破坏
- 快速的定位问题
- 通过运行单元测试定位问题
- 不会出现难以为继的现象
- 每一项功能都是测试来验证它的正确性
是一种设计行为
- 迫使我们从调用者角度考虑
- 迫使我们把程序设计成为可调用和可测试的
- 迫使我们接触模块之间的耦合
是一种编写文档的行为
- 一种无价的文档
- 类或方法如何使用的最佳文档
- 文档是可编译、可运行的
- 保持最新永远与代码同步
具有回归性
- 单元测试用例可以随时运行
- 单元测试用例可以做到自动化批量运行
- 是我们的软件能够持续构建的基石
注:3.1和3.2节参 leemingjun《单元测试的必要性和可行性 》
3.3 不写单元测试的理由
为了完成编码任务,没有足够的时间编写单元测试。编写单元测试会导致不能按时完成编码任务,推迟项目进度。
单元测试的价值不高,完全是浪费时间。
业务逻辑比较简单,不值得编写单元测试。
不知道怎么编写单元测试。
项目没有要求,所以不编写。
在项目的前期还是尽量去编写单元测试,但是越到项目的后期就越失控。
3.4 总结
在现代软件开发过程中,测试不再作为一个独立的生命周期。单元测试成为与编写代码同步进行的开发活动。单元测试能够提高程序员对程序的信心,保证程序的质量,加快软件开发速度,使程序易于维护。不管测试先行还是测试后行,没有单元测试那是绝对不行的。
注:3.3和3.4节参考袁光东在IT168上的文章《程序员为什么不写单元测试? 》
4 单元测试过程定义
5 单元测试过程活动描述
5.1 过程进入条件
实施单元测试的最佳时机是在编码阶段,若编码阶段没有进行单元测试,则也可以在后期单独开发单元测试程序。
5.2 确定目标范围
5.2.1 目的
明确进行单元测试的需求边界,确定测试目标的外延边界和内部结构。
5.2.2 活动描述
确定要进行单元测试的目标的外延边界。
若目标本身比较庞大,可能需要对目标进行分解,确定哪些部分需要进行单元测试,哪些部分无须单元测试。
5.2.3 输出
对单元测试需求的说明。
对目标边界的说明。
5.3 进行系统分析
5.3.1 目的
理解测试目标采用的技术架构,理清程序运转流程,将目标拆分成可测试的单元逻辑。
5.3.2 活动描述
理解掌握测试目标的平台架构、技术路线。
理清程序的运转流程,得出代码执行的路线图、数据流程图等。
将测试目标拆分成可测试的单元逻辑:这一步至关重要,因为只有将测试目标分解成多个相对独立的单元逻辑并确定哪些单元需要进行测试,然后才能针对具体的单元逻辑研究相应的测试方法。
推荐使用“路径法”来拆分测试目标。所谓路径法,就是在测试目标运行期间,以代码执行的路径作为单元逻辑来拆分框架的方法。我们可以数据流程图为指导,研究数据在测试目标内的流转路线,数据在测试目标内流转的路线也可以认为是程序代码的执行路径。
有些路径在系统运行期间必然会被执行到,这些路径称为“关键路径”。我们需要分析确定系统中的关键路径,即程序运行必然要调用或执行的代码,其质量必须得到确切保证,因为这部分代码一旦出错则系统必然无法运转,一般应通过严格的代码评审和代码Review的方法确保其正确性和稳定性,对其进行单元测试的优先级可以调低,无须立即着手进行。
有些路径在系统运行期间不一定总是被执行到,这些路径是在系统运行期间由系统根据实际情况动态选择执行的,因此我们把这类代码路径称为“动态路径”。动态路径是在运行期间被动态选择执行的,有些路径可能只会在特定情况下才能得到执行的机会,可能在很长时间内、多数情况下都不会执行,因此其中存在的错误或潜在的缺陷可能在很长时期内都不会直接暴露出来,即使这种错误或缺陷十分明显。如果对这些代码实行合理的单元测试,则可以及时的发现代码中存在的问题,将其消灭于初始状态,从而使我们的代码质量得到提高,并为我们后来可能对其进行的重构提供一定程度上的质量保证。因此系统中的动态路径是我们进行单元测试需要重点关注的目标,单元测试正是由于系统中动态路径的存在方能发挥其显著作用彰显其显著意义。
为了确定系统中的关键路径和动态路径,我们可以对系统进行分层抽象,明确各层的职责,各层之间相互独立,每一层只依赖于其下一层的特定接口,并为其上一层提供特定的接口。这样,将各层之间进行交互的所有接口串联起来就构成了系统的关键路径。当然,如果一个系统的代码执行路径比较简单清晰,轻易就可以确定出关键路径,那就无须进行明确的层次分割了。
关键路径是代码在层与层之间流转的路线,而动态路径一般是某一层内部的代码执行路线。当代码执行到某一层时,该层接收其下一层传递的数据,然后根据一定的逻辑选择执行其内部的某条代码分支,这样的分支就是一条动态路径。
实际上,单元测试对系统的设计间接提出了更高的要求。一个结构良好、分层清晰的系统更易于实行单元测试;相反,对结构混乱、层次混杂的系统很难写出高效的单元测试程序。并不是所有的系统都是易于测试的。
5.3.3 输出
系统分析说明,包括:系统结构图(标明程序运转路线)、关键路径与动态路径说明。
5.4 判断单元测试的必要性
5.4.1 目的
明确测试目标范围,对目标进行详细的系统分析,确定是否有必要对目标进行单元测试,或者确定目标的哪些部分需要单元测试,哪些部分不需要单元测试。
5.4.2 活动描述
可以从如下几个方面来评判单元测试的必要性:
目标代码的复杂程度
参考规模大体相当的历史项目的各项度量数据,分析没有经过单元测试的Bug数量和经过充分单元测试的Bug数量、分析项目投入的人员成本、时间成本等
其它方面
5.4.3 输出
单元测试必要性的判断结果及说明
5.5 研究测试方法
5.5.1 目的
在对测试目标详细的系统分析基础上,研究实施单元测试的具体方法,建立单元测试程序框架。
5.5.2 活动描述
针对系统的每条动态路径,研究对其实施单元测试的具体方法。
对各种测试方法进行归纳和抽象,建立编写单元测试程序的基础框架。
在基础框架的基础上,细化每种具体测试方法。
5.5.3 输出
单元测试程序基础框架及其接口说明、针对每条动态路径的测试方法说明。
5.6 执行测试
5.6.1 目的
制定测试计划,编写执行测试用例、修改发现的缺陷、总结测试结果。
5.6.2 活动描述
确定了对测试目标的测试方法后,需要根据项目进度、人力资源等方面的情况制定详细的单元测试计划,开发人员进入编码阶段,同时编写执行单元测试用例,根据单元测试暴露出的问题修改程序代码,如此不断迭代,最终完成项目开发。
项目开发完成后应该进行总结,将在开发过程中做单元测试的好的实践总结记录下来,形成持续的积累,不断完善我们的单元测试方法论。
5.6.3 输出
测试计划说明、测试用例代码、测试总结
5.7 过程输出结果
将上述各过程活动的输出结果进行归纳整理,形成相应的文档和单元测试代码库。
可以将测试目标说明、系统分析结果、测试必要性研判说明、测试方法说明、分析总结说明单独写在一份文档中,将测试计划说明、测试用例列表、用例结果记录单独写在一份文档中,也可将所有内容全部形成一份文档,根据实际情况操作即可。
我的天哪,不能不说JE的可视化编辑器让人受不了,用得超级难受,特别难以做格式。。。