软件单元测试之我见

本文是个人对于UT的一些想法和总结,参考时建议请查阅官方资料。

转载请注明出处:http://www.cnblogs.com/sizzle/p/4476392.html

测试思想

编写UT测试代码,通常是为了达到下面几个目的:

  • 在程序可以运行前确认部分模块的正确性。
  • 实行自动测试,减少人力成本。
  • 增加测试手段,降低bug到下游的概率。
  • 明确变更代码时产生的影响。

但是在实际的开发过程中,UT做成后很难达成以上目标,反而会产生一些副作用:

  • UT代码难于编写导致成本增加。
  • 使用UT检测出的bug量少,甚至在初期检测出的bug都是UT编写错误而引入的无效bug。

以上问题发生的原因是开发人员在设计和编码时没有考虑可测试性,导致UT容易发生弊大于利的情况。
在设计和编码时充分考虑可测试性,需要具备丰富的测试经验,而且难于达成,因此产生了”测试驱动开发“(Test Driven Development,简称TDD)。
TDD的测试做成过程很简单,基本可以概括为以下步骤:

  1. 根据需求和接口式样书编写测试代码,验证接下来编写的功能代码是否满足期待实现的需求点(此时的测试结果是NG)。
  2. 编写功能代码。
  3. 确认测试结果,如果NG需要修改功能代码或测试代码,如果OK则从1步骤开始实现下一个需求点或完善功能代码。

总之,TDD过程就是先写期待实现功能的测试代码,然后实装代码使测试通过的。当然,设计时是要优先考虑可测试性的,才能确保TDD过程更顺畅。
使用TDD思想开发的好处有很多,不做详细介绍,感兴趣可以baidu ”TDD 测试驱动开发“关键字,资料很多。
但是,即使利用TDD,开发过程中难免会出现一些测试相关问题:

  • UT结果NG,但是测试程序输出信息难于定位问题原因。
  • 测试条件混乱,导致UT代码频繁变更。
  • 维护人员基于代码难于分析出测试的内容。

以上问题在开发过程中难于避免,而且随着代码规模增加可能会爆发甚至导致UT被废弃不用。
为了减少该类问题发生,控制其增长的趋势,新的思想被引入到测试开发过程中--“行为驱动开发”(Behavior Driven Development,简称BDD)。
BDD其实是一种解决方案,它在TDD的基础上提出使用类似自然语言的方式描述软件的行为过程,从而使代码与需求说明更相近,可以与需求同步变化,因此代码也就容易理解和维护。
根据BDD一条Test case可以概括表示为下面三个阶段,测试代码也需要按照下面三段式上下文做成。

  1. Given:给出...前提条件。
  2. When:当执行...动作后。
  3. Then:那么应该得到...结果。

现在已经有很多基于BDD的测试框架,Java有JBehave,Ruby有RSpec和Cucumber,Object-C有Kiwi,Swift有Quick C++有CppSpec等。基于这些框架,我们可以写出优美的测试代码。下图是基于Swift语言使用Quick框架编写的测试代码以及对应的三段式上下文描述供参考:

测试范围

理论上,基于BDD开发,全部代码都需要测试,而且UT是针对“行为”,应该可以对应到明确的函数方法。但是实际开发过程中,有许多范围需要明确。

  • 类对象间存在关联,所谓“行为”具体需要涉及哪些对象。
  • 开发的功能依赖于第三方模块提供一些接口时,如何测试这些“行为”。
  • 通常执行测试的模块与被测模块是相互独立的,那么被测模块需要对外公开哪些数据和接口。

下面列举一些确定测试范围的“教条”,所谓“教条”就是只需灵活运用,不必刻意追求:

  1. 测试的“行为”要简单,也就是一条Test case的Assert最好只有一句。
  2. 基于“行为”测试,那么尽量不要公开数据,如果想要测试数据,那么应测试提供数据的方法(Swift中的只读计算属性,相当于提供数据的方法)。
  3. 测试的“行为”是内部的,第三方模块提供的接口属于外部“行为”,不需要测试。

在开发过程中,测试与设计相辅相成,设计时需要考虑程序的可测试性,测试时可以看出模块间的耦合是否过于紧密,模块的功能是否单一,从而驱动架构改善,减少项目后期因设计问题导致重构而产生的工数和关联功能间的影响。因此,在设计阶段就需要明确测试范围,规范模块间和模块内的“行为”,相应的测试代码就会简单明晰。

明确测试范围后,对于测试范围外而且依赖的对象需要做成Mocker,如下图所示:

如果测试模块B,那么需要做成Tester,以使用者A的角度测试B的“行为”是否满足期待值。其中模块C和D被B依赖,因此需要做成相应的Mocker,提供接口供B使用。Tester可以确认Mocker中收到的B传送的数据,从而测试B的“行为”是否正确。

测试观点

  • 条件测试:代码中存在条件语句时,注意多条件表达式组合情况下各种状态确认(即true与false的组合)。注意短路求值情况。
  • 边界测试:注意明确区间(闭区间,开区间,半闭半开区间),确认范围内值,边界值和越界值的情况,常见有数组,比较运算等。
  • 除零测试:注意确认除数是否可能未0。
  • 数值越界测试:注意值类型(有符号,无符号,各类型值范围)。
  • 空对象测试:注意是否存在操作空对象的情况。
  • 多返回测试:函数或语句块存在多出口时,注意考虑测试。
  • 非法参数测试:注意确认存在非法入参时,函数的动作是否正常。

测试标准

  1. 函数覆盖(Function coverage):度量程序中每个函数是否被执行到。
  2. 语句覆盖(Statement coverage):度量被测代码中每个可执行语句是否被执行到。
  3. 分支覆盖(Branch coverage):度量程序中每一个判定的分支是否都被测试到。
  4. 条件覆盖(Condition coverage):度量判定中的每个子表达式结果true和false是否被测试到

命名规范

  • 一个测试文件所包含的是一组对于行为的描述(Spec),因此习惯上使用需要测试的目标类来作为名字,并以Spec作为文件名后缀。
  • describe描述需要测试的对象内容,也即我们三段式中的Given。
  • context描述测试上下文,也就是这个测试在When来进行,一个describe可以包含多个context,来描述类在不同情景下的行为。
  • it中的是测试的本体,描述了这个测试应该满足的条件,一个context可以包含多个it的测试例。
posted on 2015-05-04 15:55  sizzle  阅读(564)  评论(0编辑  收藏  举报