欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

单元测试的含义、实践和经验

最编程 2024-04-20 16:52:54
...

单元测试的意义、做法、经验

@(工作日志)[C++|单元测试]

[TOC]

单元测试的意义

作为软件系统的最小组成单位,单元测试具有以下属性:

  • 它是由一个程序员完成的。
  • 它有一个详细的设计说明,包括输入定义、输出定义和加工说明。
  • 它是一个可识别的看得见的程序组成部分,并容易被组合成程序。
  • 能被单独地汇编和测试。
  • 它的规模比较小,逻辑比较简单。

因此单元测试具有以下意义:

  1. 单元测试集中注意力于程序的基本组成部分,首先保证每个单元测试通过,才能使下一步把单元组装成部件并测试其正确性具有基础。单元是整个软件的构成基础,像硬件系统中的零部件一样,只有保证零部件的质量,这个设备的质量才有基础,单元的质量也是整个软件质量的基础。因此,单元测试的效果会直接影响软件的后期测试,最终在很大程度上影响到产品的质量。

  2. 单元测试可以平行开展,这样可以使多人同时测试多个单元,提高了测试的效率。

  3. 单元规模较小,复杂性较低,因而发现错误后容易隔离和定位,有利于调试工作。

  4. 单元的规模和复杂性特点,使单元测试中可以使用包括白盒测试的覆盖分析在内的许多测试技术,能够进行比较充分细致的测试,是整个程序测试满足语句覆盖和分支覆盖要求的基础。

  5. 单元测试的测试效果是最显而易见的。做好单元测试,不仅后期的系统集成联调或集成测试和系统测试会很顺利,节约很多时间;而且在单元测试过程中能发现一些很深层次的问题,同时还会发现一些很容易发现而在集成测试和系统测试很难发现的问题;更重要的是单元测试不仅仅是证明这些代码做了什么,是如何做的,而且证明是否做了它该做的事情而没有做不该做的事情。

  6. 单元测试的好与坏不仅直接关系到测试成本(因为如果单元测试中易发现的问题拖到后期测试发现,那么其成本将成倍数上升),而且也会直接影响到产品质量,因为可能就是由于代码中的某一个小错误就导致了整个产品的质量降低一个指标,或者导致更严重的后果。

事实上

  • 单元测试是一种验证行为—— 测试和验证程序中每一项功能的正确性,为以后的开发提供支持;
  • 单元测试是一种设计行为—— 编写单元测试将使我们从调用者观察、思考,特别是要先考虑测试,这样就可把程序设计成易于调用和可测试的,并努力降低软件中的耦合,还可以使编码人员在编码时产生预测试,将程序的缺陷降低到最小;
  • 单元测试是一种编写文档的行为—— 是展示函数或类如何使用的最佳文档;
  • 单元测试具有回归性—— 自动化的单元测试有助于进行回归测试。

(参考:浅谈单元测试的意义)

单元测试的内容

单元测试由一组独立的测试构成,每个测试针对软件中的一个单独的程序单元。单元测试并非检查程序单元之间是否能够合作良好,而是检查单个程序单元行为是否正确。

在单元测试时,测试人员根据详细设计说明书和源程序清单,了解到该模块的I/O条件和模块的逻辑结构,主要采用白盒测试的测试用例,辅之以黑盒测试的测试用例,使之对任何合理和不合理的输入都要能鉴别和响应。这就要求对程序所有的局部和全局的数据结构、外部接口和程序代码的关键部分进行桌面检查和代码审查。

在单元测试中进行的测试工作主要在5个方面对被测模块进行检查:

模块接口测试

在单元测试开始时,应该对通过所有被测模块的数据流进行测试。如果数据不能正常地输入及输出,那么其他的全部测试都说明不了问题。Myers在关于软件测试的书中为接口测试提出了一个检查表:

  • 模块输入参数的数目是否与模块形式参数数目相同。
  • 模块各输入的参数属性与对应的形参属性是否一致。
  • 模块各输入的参数类型与对应的形参类型是否一致。
  • 传到被调用模块的实参的数目是否与被调用模块形参的数目相同。
  • 传到被调用模块的实参的属性是否与被调用模块形参的属性相同。
  • 传到被调用模块的实参的类型是否与被调用模块形参的类型相同。
  • 引用内部函数时,实参的次序和数目是否正确。
  • 是否引用了与当前入口无关的参数。
  • 用于输入的变量有没有改变。
  • 在经过不同模块时,全局变量的定义是否一致。
  • 限制条件是否以形参的形式传递。
  • 使用外部资源时,是否检查可用性并及时释放资源,如内存、文件、硬盘、端口等。

当模块通过外部设备进行输入/输出操作时,必须扩展接口测试,附加如下的测试项目:

  • 文件的属性是否正确。
  • Open与Close语句是否正确。
  • 规定的格式是否与I/O语句相符。
  • 缓冲区的大小与记录的大小是否相配合。
  • 在使用文件前,文件是否打开。
  • 文件结束的条件是否安排好了。
  • I/O错误是否检查并做了处理。
  • 在输出信息中是否有文字错误。

局部数据结构测试

模块的局部数据结构是最常见的错误来源,应设计测试用例以检查以下各种错误:

  • 不正确或不一致的数据类型说明。
  • 使用尚未赋值或尚未初始化的变量。
  • 错误的初始值或错误的默认值。
  • 变量名拼写错或书写错——使用了外部变量或函数。
  • 不一致的数据类型。
  • 全局数据对模块的影响。
  • 数组越界。
  • 非法指针。

路径测试

检查由于计算错误、判定错误、控制流错误导致的程序错误。由于在测试时不可能做到穷举测试,所以在单元测试时要根据“白盒”测试和“黑盒”测试用例设计方法设计测试用例,对模块中重要的执行路径进行测试。重要的执行路径指那些处在完成单元功能的算法、控制、数据处理等重要位置的执行路径,也指由于控制较复杂而易错的路径,有选择地对执行路径进行测试是一项重要的任务。应当设计测试用例查找由于错误的计算、不正确的比较或不正常的控制流而导致的错误,对基本执行路径和循环进行测试可发现大量的路径错误。

在路径测试中,要检查的错误有:

  • 死代码
  • 错误的计算优先级
  • 算法错误
  • 混用不同类的操作
  • 初始化不正确
  • 精度错误——比较运算错误、赋值错误
  • 表达式的不正确符号——>、>=;=、==、!=
  • 循环变量的使用错误——错误赋值等

比较操作和控制流向紧密相关,测试用例设计需要注意发现比较操作的错误:

  • 不同数据类型的比较。
  • 不正确的逻辑运算符或优先次序。
  • 因浮点运算精度问题而造成的两值比较不等。
  • 关系表达式中不正确的变量和比较符。
  • “差 1 错”,即不正常的或不存在的循环中的条件。
  • 当遇到发散的循环时无法跳出循环。
  • 当遇到发散的迭代时不能终止循环。
  • 错误的修改循环变量。

错误处理测试

错误处理路径是可能引发错误处理的路径及进行错误处理的路径,错误出现时错误处理程序重新安排执行路线,或通知用户处理,或干脆停止执行使程序进入一种安全等待状态。测试人员应意识到,每一行程序代码都可能执行到,不能自己认为错误发生的概率很小而不去进行测试。一般软件错误处理测试应考虑下面几种可能的错误:

  • 出错的描述是否难以理解,是否能够对错误定位。
  • 显示的错误与实际的错误是否相符;
  • 对错误条件的处理正确与否;
  • 在对错误进行处理之前,错误条件是否已经引起系统的干预等。

在进行错误处理测试时,要检查如下内容:

  • 在资源使用前后或其他模块使用前后,程序是否进行错误出现检查。
  • 出现错误后,是否可以进行错误处理,如引发错误、通知用户、进行记录。
  • 在系统干预前,错误处理是否有效,报告和记录的错误是否真实详细。

边界测试

边界测试是单元测试中最后的任务。软件常常在边界上出错,例如,在一个程序段中有一个n次循环,当到达第n次循环时就可能会出错;或者在一个有n个元素的数组中,第n个元素时是很容易出错的。因此,要特别注意数据流、控制流中刚好等于、大于或小于确定的比较值时出错的可能性。对这些地方要仔细地选择测试用例,认真加以测试。

此外,如果对模块性能有要求的话,还要专门进行关键路径测试,以确定最坏情况下和平均意义下影响运行时间的因素。下面是边界测试的具体要检查的内容:

  • 普通合法数据是否正确处理。
  • 普通非法数据是否正确处理。
  • 边界内最接近边界的(合法)数据是否正确处理。
  • 边界外最接近边界的(非法)数据是否正确处理等。
  • 在n次循环的第0次、第1次、第n次是否有错误。
  • 运算或判断中取最大最小值时是否有错误;
  • 数据流、控制流中刚好等于、大于、小于确定的比较值时是否出现错误。

单元测试的要求

为了使单元测试能充分细致地展开,应在实施单元测试中遵守下述要求:

语句覆盖达到99%

语句覆盖指被测单元中每条可执行语句都被测试用例所覆盖。语句覆盖是强度最低的覆盖要求,要考虑语句覆盖的意义,只要想象一下用一段从没执行过的程序控制庞大的飞行器升上天空,然后设法使它精确入轨,这种轻率简直就是荒唐。实际测试中,不一定能做到每条语句都执行到。第一,存在“死码”,即由于程序设计错误在任何情况下都不可能执行到的代码。第二,不是“死码”,但是由于要求的测试输入及条件非常难达到或单元测试的条件所限,使得代码没有得到运行。因此,在可执行语句未得到执行时,要深入程序作详细的分析。如果是属于以上两种情况,则可以认为完成了覆盖,但是对于后者,如果可能一定要尽量测试到,如果以上两者都不是,则是因为测试用例设计不充分,需要再设计测试用例。

分支覆盖达到100%

分支覆盖指分支语句取真值和取假值各一次,分支语句是程序控制流的重要处理语句,在不同流向上测试可以验证这些控制流向的正确性。分支覆盖使这些分支产生的输出都得到验证,提高测试的充分性。

覆盖错误处理路径

单元的软件特性覆盖

软件的特性包括功能、性能、属性、设计约束、状态数目、分支的行数等。

对试用额定数据值、奇异数据值和边界值的计算进行检验,用假想的数据类型和数据值运行,测试排斥不规则输入的能力。

单元测试通常是由编写程序的人自己完成的,但是项目负责人应当关心测试的结果。所有的测试用例和测试结果都是模块开发的重要资料,需妥善保存。

(参考:详细讲解单元测试的内容)

实践经验

单元测试的时机很重要

无非两种:

  1. 在具体实现代码之前,这就是所谓的测试驱动开发;
  2. 与具体实现代码同步进行,正是大部分人采用的方式。
    那种事后单元测试,基本是没用的。当然有一种例外:要对没有单元测试的既有代码进行维护和改造,这时候需要为既有代码追加单元测试,但是这也得建立在充分调查理解需求的基础上才能进行。

单元测试的执行者的角色

单元测试应当由具体实现代码的开发者进行,也就是说每个开发人员都应当同时对自己负责具体实现代码和单元测试代码负责。
这里也存在同第一条中的例外的情况。

单元测试应当突出重点

应当对那些重点部分重点关照,主要有:

  1. 逻辑复杂的
  2. 容易出错的
  3. 不易理解的,即使是自己过段时间也会遗忘的,看不懂自己的代码,单元测试代码有助于理解代码的功能和需求
  4. 后期需求变更可能性相对比较大的,这样后期需求更变修改代码之后就不用太担心写的代码对不对以及是否破坏既存代码逻辑了

单元测试也应注重质量,不要写无用的测试代码

写没有实际用处的单元测试不如不写,正如注释,写没有意义的注释也不如不写,既降低代码可读性又容易误人子弟。

敏捷开发的宗旨是“以人为本”,是解放,而不是压迫

单元测试虽说从长期来看,可以提高代码质量、减少维护成本、降低重构难度,拥有众多好处。但是从短期来看,肯定是会加大工作量的,也就是说需要投入更多的人力成本,如果只想得到好处,却不愿投入相应的成本,还是不要搞单元测试了——那只会导致开发人员更多的加班,不会产生好的效果。因为整个敏捷开发的宗旨,是“以人为本”,而不是从开发人员身上榨出更多的油水。单元测试是为了解放开发人员,而不是压迫,是为了从长远的角度减轻开发人员的工作量!

最后要说的是:
那种为了单元测试而单元测试的愚蠢行为应当立即停止。
那种只是想把单元测试作为一项面子工程的行为更应当停止(官场的种种坏习惯不应该在思想纯洁的程序员当中流行)。
那些对单元测试没有深入理解,只是希望今后能冠以“单元测试覆盖率100%”荣誉头衔的团队,应该立即停止这种想法。
单元测试不应当过于重视覆盖率,而应该在需要的时候写单元测试。何时写,怎么写,都需要建立在开发者已经对单元测试有深刻理解的基础上。
单元测试也是一把双刃剑,要用得好它才能发光发热,产生强大的正能量,请不要把它当作“龙泉宝剑”挂在自家的玄关辟邪

(参考:对单元测试的一点感悟)


(参考:Geotechnical 单元测试准则 - 中文版)

推荐阅读