【腾讯TMQ】实践单元测试的姿势

# 引言 #:单元测试的目的是什么呢?就是完整检测代码单元的功能逻辑,找出代码单元本身的所有功能逻辑错误,具体来说,就是检测对数据的各种分支是否考虑全面,处理是否正确。形象地说,单元测试的目的就是验证:无论别人怎么样,我总是对的。“别人”,是指相关代码或环境,“我”,是指正在编写或测试的代码单元。

单元测试为啥能提高代码质量呢?由于每个单元有独立的逻辑,做单元测试时需要隔离外部依赖,确保这些依赖不影响验证逻辑。因为要把各种依赖分离,单元测试会促进工程进行组件拆分,整理工程依赖关系,更大程度减少代码耦合。这样写出来的代码,更好维护,更好扩展,从而提高代码质量。
那么我们应该如何编写单元测试的代码?当遇到被测代码可测性差时如何解决?本文试着从个人实践出发来阐述这两个问题。

姿势1: 3A原则组织单元测试

单元测试都有相同的流程。首先需要设置好合适的条件,然后执行代表要验证的行为的代码,最后验证结果是否和预期的一样。

测试应当尽可能地直接反映其测试意图。这就意味着阅读测试代码的人不需要细细品读代码中的每一行,就能很容易的理解测试的基本构成:单元测试的初始化(Arrange)、测试的行为(Action)、以及怎样验证测试结果(Assert)。

Arrange、Action、Assert提醒你直观地去组织单元测试以便能够快速的阅读代码。

1、Arrange初始化

单元测试中的初始化工作,是为了解决被测函数中必要的前置依赖的问题。如下例所示:

上述代码均对测试类CRewWellCollection进行初始化添加数据的工作,方才可以验证CRewWellCollection对应成员函数的功能是否符合预期。
大多数单元测试工具都支持将逻辑上的相关的测试分组。在google mock,可以使用google所谓的测试用例名称(fixture)来将相关的测试分组。如果测试用例中的所有测试需要一条或更多的相同初始化语句,那么可以将他们写在fixture类的初始化函数中。在google mock中必须将此函数命名为SetUp(它覆写了基类::testing::Test中的虚函数)。如下所示:

将重复的初始化工作,放到同一个fixture类中,让测试用例目的更突出。

2、Action行为动作

单元测试主要对模块的五个基本特性进行行为的评价:

(1)模块接口测试从以下几点考虑行为手段:

1)调用本模块的输入参数是否正确;

2)全局数据结构是否有问题,保证系统数据的正确性;

3)模块的误差积累起来,是否会放大,从而达到不可接受的程度,确保误差不影响系统功 能及性能。

(2)模块内部数据测试从以下几点考虑行为手段:

1)变量是否有正确初始化;

2)数组越界;

3)非法指针。

(3)错误异常处理从以下几点考虑行为手段:

1) 是否检查错误出现;

2)出现错误,是否进行错误处理。抛出错误、通知用户、进行记录;

3) 错误处理是否有效;

(4)边界条件测试从以下几点考虑行为手段:

1)普通合法数据是否正确处理;

2)普通非法数据是否正确处理;

3) 边界内最接近边界的(合法)数据是否正确处理;

4)边界外最接近边界的(非法)数据是否正确处理。

(5)独立执行路径测试从以下几点考虑行为手段:

1)死代码;

2)精度错误(比较运算错误、赋值错误);

3)表达式的不正确符号。

单元测试从上述五个行为出发,来验证代码所对应的目的与预期。

3、Assert断言

断言可以将一个普通的测试转变成自动化的测试。如果没有断言,那么单测只是执行了一段代码而已。如果想要验证一段代码是否正确工作,则需要人工查看结果。人工验证测试结果是耗时的。断言可以帮助我们自动化的验证结果。

当测试框架运行单个测试时,它会从头到尾执行测试代码段中的语句。每遇到一个断言,都意味着要去验证一些期待的结果。如果断言的条件不满足,那么测试框架就会终止测试。测试框架会保存测试失败的信息,运行teardown逻辑,然后接着运行下一个测试。

断言让单元测试拥有了自动化测试的能力。

姿势2:干掉单元测试的天敌—可测性

单元测试效益特别高,方法看起来也很简单,但却尝试的多,成功实施的少,为什么呢?主要原因在于难于突破可测性问题。“可测”这个词,意思已经很明白了,如果不“可测”的话,那就是不能测,没法测,就是做不下去;或者困难太多,成本太重,热情被逐渐消磨,最后做不下去。所以可测性问题是单元测试的关键,是我们首先要解决的。

为什么代码会不可测呢?一般来说,这些原因导致了代码的可测性差:项目很复杂,开发流程不规范,耦合度很高。耦合是指代码之间的互相依赖,例如一个函数调用另一个函数,就是耦合。

流行的说法是改进开发流程,提高代码可测性,但从实践来看,这是不现实的。可测性差在项目中普遍存在,有其客观原因,很难改变:

首先,项目本身就大多是很复杂的,这由需求决定,改不了。

其次,程序并不是虚无的,程序是客观事物的反映,客观事物本身是互相关联,互相纠缠的,必然形成代码间的耦合。

第三,流程改进是一个长期的、渐进、困难的过程,不可能短期内实现飞跃,更不是引进几个工具或者规范就可以做到的。

如何解决可测性问题?可以从测试技术的角度来考虑。要解决问题,首先要对问题有充分的了解。一个函数要“可测”,要做到两方面:第一是能够独立运行,第二是要能够覆盖输入分类。为什么要覆盖输入分类呢?因为单元测试的目标是覆盖代码单元的功能逻辑,要做到覆盖功能逻辑,就要覆盖输入的所有分类。

1、独立运行(隔离、解依赖);

要进行单元测试,首先就是要将被测试的单元,与所有外部依赖进行隔离。
隔离独立运行包括两点:一是与其他代码隔离,二是与依赖系统隔离。

与其他代码隔离的一般方式是mock,mock就用简单代码代替实际的代码,例如函数A调用了函数B,函数B又调用了函数C和函数F,如果函数B用mock来代替,那么,函数A就可以完全切断与函数C和函数F的关系。与依赖系统隔离常见于跨平台测试,例如在PC上测试嵌入式项目。这要解决两个问题:编译差异和平台差异。编译差异主要是语法上的差别,例如,有些开发环境定义了非标准的关键字。平台差异主要表现在个别数据长度在不同平台上的不一致。

为了方便实现隔离,对软件设计和开发的一个潜在的激励是,软件模块的设计人员和开发人员,不得不时时思考,在当前语言支持的各种特性的条件下,如何使得所写的代码,能够被方便的被隔离。虽然这看起来很像是一种限制,但是和软件设计的SOLID原则(面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写),其实是不谋而合的,因此,也就未尝不是一个优点。

2、覆盖输入分类

一个函数,输入会有哪些呢?输入包括两方面:外部输入,内部输入。外部输入容易理解,就是函数外部可以设定的输入,包括参数,全局变量,成员变量。关键是内部输入。什么是内部输入?

一个函数,对于调用底层函数获得的数据,是如何处理的呢?跟参数一样,也是分类处理。所以,测试时也要分类检测,这与参数没什么区别,这就是内部输入。内部输入就是函数内部取得的数据,除调用底层函数取得的内部输入外,常见的还有局变静态变量。

3、可测性梳理

针对代码的耦合依赖、分类进行覆盖测试,遇到的各种代码可测性的坑,本质上是代码自身产生的原因。那么如何提高代码的可测性(高质量代码),并可以覆盖所有输入分类呢?下面列出的点是经过过滤总结相对简单易接受的可提供代码可测性的规则:


总结

单元测试保障工程各个“零件”按“规格”(需求)执行,从而保证整个“机器”(项目)运行正确,最大限度减少bug。

按照Arrange-Action-Assert的3A原则可以让我们单元测试的代码组织简单易懂,直接反映出测试意图。代码做不到单元测试,可测性差时,多思考如何改进,而不是放弃。

当单元测试成为我们自身的Owner时,任何关于单元测试的负面因素都已经不是问题。为啥?因为这已经深入灵魂,成为一个标准的程序员每天需要的常态工作。

关注微信公众号:腾讯移动品质中心TMQ,获取更多测试干货!

【腾讯TMQ】实践单元测试的姿势_第1张图片

版权所属,禁止转载

你可能感兴趣的:(单元测试)