单元测试常见问题及测试方法

  黑盒测试由于看不到代码内部的实现逻辑,经常出现漏测或测试不到位的情况,因为有些潜在的问题通过黑盒测试不好复现,但code review就很容易发现问题所在,那我们在code review时应该注意哪些点呢?

首先了解单元测试中驱动代码,桩代码和Mock代码三者的逻辑关系

单元测试常见问题及测试方法_第1张图片
单元测试常用方法.png

驱动代码(Driver)指调用被测函数的代码:单元测试中,驱动模块通常包括调用被测函数前的数据准备、调用被测函数及验证相关结果三个步骤。
桩代码(Stub)是用来代替真实代码的临时代码。比如,某个函数A的内部实现中 调用了一个尚未实现的函数B,为了对函数A的逻辑进行测试,那么就需要模拟一个函数B,这个模拟的函数B的实现就是所谓的桩代码。
ex:假定函数A是被测函数,其内部调用了函数B(伪代码如下):
单元测试常见问题及测试方法_第2张图片
A函数调用B函数.png

被测函数A内部调用了函数B
(在单元测试阶段,由于函数B尚未实现,但为了不影响对函数A自身逻辑的测试,我们可以用一个假函数B来代替真实函数B,那么假函数B就是桩函数)

  为了实现函数A的全路径覆盖,我们需要控制不同的测试用例中函数B的返回值,那么桩函数B的伪代码就应该是这个样子:


单元测试常见问题及测试方法_第3张图片
装代码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、语法特征错误:从编程语法上就能发现的错误,如:不符合编程语言语法的语句

单元测试常见问题及测试方法_第4张图片
数组越界.png

  数组越界,访问了未被初始化的内存空间,代码运行时就会造成意想不到的结果。黑盒测试还不一定测得到,比如我们测试有遇到的数据越界问题:删除购物车商品偶尔会造成程序闪退,但从黑盒测试上是不能必现的,只能从代码层面去找问题

  2. 边界值行为特征错误:代码在执行过程中发生异常,崩溃或者超时,此类错误通常发生在一些边界条件上


单元测试常见问题及测试方法_第5张图片
边界值特征错误.png

  以上代码就存在具有边界行为特征错误。当b取值为0时,Division函数就会抛出运行时异常

  3.经验特征错误:根据过往经验发现代码错误


单元测试常见问题及测试方法_第6张图片
经验特征错误.png

  代码想要表达的意思:如果变量i的值等于2,就调用函数operationA,否则调用函数operationB。

  但是,代码中将“if(i==2)" 错误地写成了" if(i=2)",就会使原本的逻辑判断操作变成赋值操作,而且这个赋值操作的返回结果永远是true,即这端代码永远只会调用operationA的分支。

  显然,“if(i=2)”在语法上没有错误,但是从过往经验来看,这就很可能是个错误了

  4、算法错误:代码完成的计算(或则功能)和之前预先设计的计算结果(或则功能)不一致。
这类错误直接关系到代码需要实现的业务逻辑,在整个代码级测试中所占比重最大,也是最重要的。但是,完全的算法错误不常见,因为不能准备完成基本功能需求的代码,是一定不会被提交的。所以,项目中最常见的是部分算法错误。

  5、部分算法错误:在一些特定条件或则输入情况下,算法不能准确完成业务要求实现的功能。


单元测试常见问题及测试方法_第7张图片
部分代码错误.png

  这段代码,完成了两个int类型整数的加法运算。在大多数情况下,这段代码的功能逻辑都是正确的,能够准确地返回两个整数的加法之和,但是,在某些情况下,可能存在两个很大的整数相加后“和"越界的情况,也就是说两个很大的int数相加的结果超过了int的范围。这是典型的部分算法错误。

代码级测试常用方法

单元测试常见问题及测试方法_第8张图片
代码级测试常用方法.jpg

自动静态方法能够以极低的成本发现以下问题:

  • 使用未初始化的变量;
  • 变量在使用前未定义;
  • 变量声明了但未使用;
  • 变量类型不匹配;
  • 部分内存泄漏的问题;
  • 空指针引用;
  • 缓冲区溢出;
  • 数组越界;
  • 不可达的僵尸代码;
  • 过高的代码复杂度;
  • 死循环;
  • 大量的重复代码块;
    ....
    实现方式:企业结合自己的编码规范定制度规程库,并于本地的IDE开发环境和持续集成流水线整合

代码本地开发阶段,IDE环境就可以自动对代码实现自动静态检查;当代码提交到仓库后,CI/CD流水线自动触发代码静态检查,如果检查到潜在错误,就会自动发邮件通知代码递交者。

  如图:C语言代码存在数组越界的问题,通过C语言的自动静态扫描工具splint发现这个问题,并给出分析结果。

单元测试常见问题及测试方法_第9张图片
静态扫描结果.png

动态测试方法也就是单元测试方法,看似简单,但在实际工程中会遇到很多困难:
  1、单元测试用例”输入参数“的复杂性,表现在”输入参数“不是简单的函数输入参数。本质上,任何能够影响代码执行路径的参数,都是被测函数的输入参数。
  2、单元测试用例”预期输出“的复杂性,主要表现在”预期输出“应该包括被测函数执行完成后所改写的所有数据
  3、关联代码不可用,需要采用桩代码模拟不可用代码,并通过打桩补齐未定义部分

单元测试用例 ”输入参数“的复杂性
函数内部调用子函数获得数据

单元测试常见问题及测试方法_第10张图片
函数内部调用子函数.png

  函数 Func_SUT 是被测函数,它的内部调用了函数 FuncX,函数 FuncX 的返回值是 bool 类型,并赋值给内部变量toggle,之后代码会根据变量toggle的取值来决定执行哪个代码分支。那么,被测函数内部调用子函数获得的数据也是单元测试的输入参数。

你可能感兴趣的:(单元测试常见问题及测试方法)