黑盒测试由于看不到代码内部的实现逻辑,经常出现漏测或测试不到位的情况,因为有些潜在的问题通过黑盒测试不好复现,但code review就很容易发现问题所在,那我们在code review时应该注意哪些点呢?
首先了解单元测试中驱动代码,桩代码和Mock代码三者的逻辑关系
驱动代码(Driver)指调用被测函数的代码:单元测试中,驱动模块通常包括调用被测函数前的数据准备、调用被测函数及验证相关结果三个步骤。
桩代码(Stub)是用来代替真实代码的临时代码。比如,某个函数A的内部实现中 调用了一个尚未实现的函数B,为了对函数A的逻辑进行测试,那么就需要模拟一个函数B,这个模拟的函数B的实现就是所谓的桩代码。
ex:假定函数A是被测函数,其内部调用了函数B(伪代码如下):
A函数调用B函数.png
被测函数A内部调用了函数B
(在单元测试阶段,由于函数B尚未实现,但为了不影响对函数A自身逻辑的测试,我们可以用一个假函数B来代替真实函数B,那么假函数B就是桩函数)
为了实现函数A的全路径覆盖,我们需要控制不同的测试用例中函数B的返回值,那么桩函数B的伪代码就应该是这个样子:
装代码B函数.jpg
这样就覆盖了被测函数A的if-else的两个分支
(当执行第一个测试用例的时候,桩函数B应该返回true,而当执行第二个测试用例的时候,桩函数B应该返回false)
桩代码的作用:起到隔离和补齐的作用,使被测代码能够独立编译、链接,并独立运行。同时,桩代码还具有控制被测函数执行路径作用
Mock代码和桩代码非常相似,都是用来代替真实代码的临时代码,起到隔离和补齐的作用
Mock代码和桩代码本质区别:测试期待结果的验证(Assert and Expectiation)
- 对于Mock代码来说,我们关注点是Mock方法有没有被调用,以什么样的参数被调用,被调用的次数,以及多个Mock函数的先后调用顺序,所以,在使用Mock代码的测试中,对于结果的验证(也就是assert),通常出现在Mock函数中
- 对于桩代码来说,我们关注点是利用Stub来控制被测函数的执行路径,不会去关注Stub是否被调用以及怎样被调用。所以,在使用Stub的测试中,对于结果验证(也就是assert),通常出现在驱动代码中
就算不能分清Mock代码和桩代码,也不影响单元测试。深入比较,参考 马丁.福勒(Martin Fowler)的著名文章《Mock代码不是桩代码》
实际项目中如何开展单元测试?
1、并不是所有代码都要进行单元测试,通常只有底层模块或则核心模块的测试才会采用单元测试
2、确定单元测试框架选型,这和开发语言有关。比如Java最长用的单元测试框架是Junit 和TestNG
3、为了能够衡量单元测试的代码覆盖率,需要引入代码覆盖率工具。不同的语言会有不同的代码覆盖率工具。比如Java的JaCoCo,JavasScipt的Istanbul
代码覆盖率工具的实现原理
实现代码覆盖率的统计,最基本的方法就是注入,在被测代码中自动插入用户覆盖率统计的探针(Proble)代码,并保证插入的探针代码不会给源代码带来任何影响
常见的代码级错误
1、语法特征错误:从编程语法上就能发现的错误,如:不符合编程语言语法的语句
数组越界,访问了未被初始化的内存空间,代码运行时就会造成意想不到的结果。黑盒测试还不一定测得到,比如我们测试有遇到的数据越界问题:删除购物车商品偶尔会造成程序闪退,但从黑盒测试上是不能必现的,只能从代码层面去找问题
2. 边界值行为特征错误:代码在执行过程中发生异常,崩溃或者超时,此类错误通常发生在一些边界条件上
边界值特征错误.png
以上代码就存在具有边界行为特征错误。当b取值为0时,Division函数就会抛出运行时异常
3.经验特征错误:根据过往经验发现代码错误
经验特征错误.png
代码想要表达的意思:如果变量i的值等于2,就调用函数operationA,否则调用函数operationB。
但是,代码中将“if(i==2)" 错误地写成了" if(i=2)",就会使原本的逻辑判断操作变成赋值操作,而且这个赋值操作的返回结果永远是true,即这端代码永远只会调用operationA的分支。
显然,“if(i=2)”在语法上没有错误,但是从过往经验来看,这就很可能是个错误了
4、算法错误:代码完成的计算(或则功能)和之前预先设计的计算结果(或则功能)不一致。
这类错误直接关系到代码需要实现的业务逻辑,在整个代码级测试中所占比重最大,也是最重要的。但是,完全的算法错误不常见,因为不能准备完成基本功能需求的代码,是一定不会被提交的。所以,项目中最常见的是部分算法错误。
5、部分算法错误:在一些特定条件或则输入情况下,算法不能准确完成业务要求实现的功能。
部分代码错误.png
这段代码,完成了两个int类型整数的加法运算。在大多数情况下,这段代码的功能逻辑都是正确的,能够准确地返回两个整数的加法之和,但是,在某些情况下,可能存在两个很大的整数相加后“和"越界的情况,也就是说两个很大的int数相加的结果超过了int的范围。这是典型的部分算法错误。
代码级测试常用方法
自动静态方法能够以极低的成本发现以下问题:
- 使用未初始化的变量;
- 变量在使用前未定义;
- 变量声明了但未使用;
- 变量类型不匹配;
- 部分内存泄漏的问题;
- 空指针引用;
- 缓冲区溢出;
- 数组越界;
- 不可达的僵尸代码;
- 过高的代码复杂度;
- 死循环;
- 大量的重复代码块;
....
实现方式:企业结合自己的编码规范定制度规程库,并于本地的IDE开发环境和持续集成流水线整合
代码本地开发阶段,IDE环境就可以自动对代码实现自动静态检查;当代码提交到仓库后,CI/CD流水线自动触发代码静态检查,如果检查到潜在错误,就会自动发邮件通知代码递交者。
如图:C语言代码存在数组越界的问题,通过C语言的自动静态扫描工具splint发现这个问题,并给出分析结果。
动态测试方法也就是单元测试方法,看似简单,但在实际工程中会遇到很多困难:
1、单元测试用例”输入参数“的复杂性,表现在”输入参数“不是简单的函数输入参数。本质上,任何能够影响代码执行路径的参数,都是被测函数的输入参数。
2、单元测试用例”预期输出“的复杂性,主要表现在”预期输出“应该包括被测函数执行完成后所改写的所有数据
3、关联代码不可用,需要采用桩代码模拟不可用代码,并通过打桩补齐未定义部分
单元测试用例 ”输入参数“的复杂性
函数内部调用子函数获得数据
函数 Func_SUT 是被测函数,它的内部调用了函数 FuncX,函数 FuncX 的返回值是 bool 类型,并赋值给内部变量toggle,之后代码会根据变量toggle的取值来决定执行哪个代码分支。那么,被测函数内部调用子函数获得的数据也是单元测试的输入参数。