Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1347308
  • 博文数量: 118
  • 博客积分: 3888
  • 博客等级: 中校
  • 技术积分: 2940
  • 用 户 组: 普通用户
  • 注册时间: 2007-02-10 18:15
个人简介

一看二做三总结

文章分类

全部博文(118)

分类: C/C++

2011-12-07 16:43:56

1.1      单元测试定义

单元测试是在软件开发过程中要进行的最低级别的测试活动,在单元测试活动中,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。 单元测试不仅仅是作为无错编码一种辅助手段在一次性的开发过程中使用,它还是保证软件升级与优化的最基本手段,因此在整个软件周期中,都必须维护。

在结构化语言中,比如C,进行测试的单元一般是函数。在象C++这样的面向对象语言中, 进行测试的基本单元是类。

单元测试(模块测试) 是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定 函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。

单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。

其实我们每天都在做单元测试。一个函数,总是要执行一下,看看功能是否 正常,有时还要想办法输出些数据,如弹出信息窗口什么的,这也是单元测试,被称为临时单元测试。

临时单元测试的问题是没有连贯性,很多测试都会丢失,代码有改动或优化后测试代码还要重复开发。还有就是没有全局眼光,会有很多分支被遗漏,而这些地方往往会隐藏大量bug

1.2 单元测试优点 1.2.1      设定目标

我们编写每一个函数都是有目标的,一般都是要完成一种功能,而测试代码是这种目的的最好体现。比如要实现一个加法函数int A(int a, int b),目标是实现两个整数相加,该目标具体到测试函数就是ASSERT(33=A(11, 22))。我们的代码只要能通过该测试,则说明成功;否则必修修改代码了。

总的来说,就是明确了我们的目标,不至于在设计中跑题。

1.2.2      优化设计

编写单元测试强迫我们从调用者观察、思考。迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。

好代码的指标之一是可测性。写过单元测试用例的童鞋都有这样的感觉,很多代码的测试用例非常难写。通过让开发人员做单元测试,可以让我们在设计代码之初就考虑到代码的可测性,迫使我们对代码结构的优化作更多的思考。

1.2.3      最优文档

我们花了那么多时间写接口文档,但总是落后于代码的改动。而单元测试的好处是永远与代码同步,因为它与代码是相互验证的。单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。

1.2.4      回归测试

对大部分非软件人员来说,软件开发的成本很低。常听到的说法是:不就是加几行代码吗?有什么不能加的!。这就导致了软件需求的变动次数要远远大于硬件。随之而来的就是无数次的升级与重构,谁也不敢保证这次修改的代码不会破坏之前的功能,代码变得越来越难以维 护,质量也越来越差。随之而来的就是项目的崩溃与拖延。

单元测试时解决这一问题的方法之一。程序的每一项功能都有测试方法验证它的正确性。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏原有功能。

1.3 单元测试缺点

编写单元测试涉及的技术很多,如果只是单纯的使用Junit或是TestNG这样的基础单元测试框架往往很难应对各种复杂的单 元测试情况,所以势必要借助很多第三方的框架和技术,这些框架和技术的学习会增加学习的成本和难 度。

1.3.2      会增加程序员工作量

单元测试跟生产代码是一样的,并不会应为是用来测试的就有所不同,开发人员同样要面对测试代码的编写、维护等工作,也同样要面对避免重复代码等一系列问题,能否写出好的测试代码还是取决于开发人员的设计和编码能力。

1.3.3      推广和运用需要比较大投入

只有在每个开发人员都编写了足够的、质量好的单元测试代码,大家才能真正享受到单元测试带给我们的好处。在达到这种层度以前,还需要不少实现和资源的投入。

学习成本算问题吗?学习编程成本更高。

相对而言单元测试的学习难度还是比较低的,作为一名合格的程序员,相信很快究就可以掌握。而且这是一次辛苦,永远受益的工作,值得花时间学习。

后两个其实是一个问题。编写单元测试用例会增加工作量,延长开发周期。

从我的经验看,后期维护花费的时间是惊人的。尤其是原来的 开发人员已经离开,换其他人维护时。程序员小心的修改代码,战战兢兢的合入,然后就是漫长的等待着审判的降临。经常是用两分钟修改代码,用两天时间测试代码。

假如开发周期为3,原来的方案是开发用1、测试用2。假如单元测试后,方案变为开发与单元测试用2、测试用1。由于单元测试确实会增加工作量,开发周期可能会超过3。但换来的是日后升级时间的大量节省与项目稳定周期的大大缩短。

 

对于已经稳定的部分,类似于第三方包,平台部分,其至是遗留系统中已经证明是可靠的部分,都可以信任。这些是我们用例代码依赖的部分,是我们用来检验其它待测部分的基石。如果什么都要测,就会变成什么都测不了。

测试范围通常是包括我们编写的所有代码。如果编写的是大系统中的一个子系统,则该子系统为测试范围,通常称为harness

1.4.2      基本测试思路

单元测试的目的是测试某个函数是否达到设计要求,因此每次只测一个函数。对于该函数内部调用的其他函数的情况。如果是库函数则直接用,因为库函数是默认正确的,个别情况为了控制跳转方向,可以打桩。如果是harnness中的其他函数,则完全打桩。因为它未经过测试,是不可信的。

1.4.3      覆盖率(coverage

覆盖率是单元测试的一个重要指标,通常有下面三种覆盖率:

Statement: reports on each executable statement

Branch: reports on the outcome of each decision point

MC/DC: report on branch outcomes as well as equivalence paires

以下面的代码为例:

a = 1;

If ((2 != b) || (3 != c))

{

    a = 0;

}

Statement要求覆盖到所有可执行语句。例子中字需要一个test case就可以覆盖所有语句。

Branch要求每册判断条件都要测到。比如if true与为false两个分支都要执行到,因此需要两个test case,分别测试if的两种情况。

MC/DC要求在branch的基础上每个分支的每个条件都要测试到。前提是该条件可以独立影响代码执行顺序。例子中的代码需要三个分支可以覆盖。(a==2 and b==3), (a==2 and b!=3), (a!=2 and b==3)。注意(a!=2 and b!=3)的情况不需要测试分支,因为任意改变其中一个条件都无法独立影响结果。

1.4.4      边界测试(boundary)

边界问题很常见,如数组的最后一个元素越界等。因此测试用例需要包括输入的每个边界。如下列代码:

If (1 < a)

{

    ……

}

最佳的选择是(a==1), (a==2)两种情况。假如选择(a==0), (a==3),从覆盖率角度看是一样的,但显然效果不好。因为假如吧条件改为“2 < a”同样能通过测试。

1.4.5      Output判断

覆盖率与边界测试的目的是保证不遗留任何场景,而测试的目的是保证函数按设计要求工作,因此执行结果的判断至关重要。

对结果的判定并不容易,其难易程度通常是由代码的实现方式决定的,因此并没有一个固定的方法。

1.4.6      保证测试方法简单

如果连测试方法都很复杂,难道我们还要再写测试用例来保证它的正确执行不成?这样岂不是麻烦大了!所以,测试方法一定要写的尽可能的简单,写到你认为白痴都能看懂的程度。

测试方法复杂的原因通常是判断output的难度与构造前置条件的困难。因此本条更依赖于代码的实现。

1.5 如何实现代码的可维可测

代码要首先可测,然后才能测。

单元测试是白盒测试,测试过程与代码实现关系很大。不好的代码设计会导致测试难度的增加,并影响测试效果。不过幸好单元测试执行者通常也是代码开发者。开发人员最好在实现函数之前,至少也是实现代码时,就考虑好如何编写测试用例。这也从另一个角度约束我们的编码行为,更有利于代码质量的提高。

面对着一个方法,对于如何写测试用例你感到一筹莫展。并不是你的错,而是因为这个方法很烂。测一个方法就是在使用这个方法,你自己都这样无奈,将来真正使用的人岂不是要骂人了。这个时候,重构一下很值得。

下面是列举了几条代码编写规则.

1、方法定义具备原子性, 一个逻辑修改应该只涉及到一个方法, 如果要改多处地方, 那么,第一时间就要反思它们是否还可以抽离成一个新方法,以唯一的支持这个逻辑的变动。

2、方法参数尽可能简单, 参数个数尽可能少,最好所有的参数都是简单数据类型, 原因很简单, 如果调来调去的都是一个一个笨重的对象, 那在写单元测试时,参数构造非常麻烦。

3、使用模块化方法,编码低耦合、高内聚。耦合性低是所有软件开发的根本原则了, 但他在代码可测试性中也是非常重要的。更低的耦合性,能够保证在测试时更容易的定位问题。

4、所有的方法都应该在某个地方有输出。从测试的角度上讲,没有输入的方法是无效的方法。为什么说某个地方而不是自己有输出,是因为我们都知道, 在应用中有很多方法是纯业务流方法。它改变了很多( )类的属性,而本身却不需要有任何的反映, 自己不需要或不方便提供返回值。但是, 我们知道,恰恰是这些方法,是最容易发生修改的。 那么,如何保证它们都正确执行呢?首先是考虑它是否改变了某些全局变量,我们能够通过判断全局变量或者通过某些接口对其结果进行判断。否则,至少也要考虑给这个方法提供必须的返回值,以反映执行完 这个方法的结果。

5面向切面编程。给我们提供的启示是,将真正需要测的逻辑分离出来。摆脱那些无意义且简单重复的代码对测试的干扰。

6、明确的契约方法。一定要有明确清晰的输入/输出。建议在方法的注释描述中,分三段描述”“前置条件”“后置条件

本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接。内容可任意使用,但对因使用该内容引起的后果不做任何保证。
作者:fireaxe_hq@hotmail.com
博客:fireaxe.blog.chinaunix.net

阅读(4649) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~