什么是单元测试

概念

单元测试:是指对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作,这里的最小可测试单元通常是指函数或者类。

为什么要写单元测试?

一谈到单测,可能大家的第一反应都是敬而远之。

没啥用,没时间,我不会

我承认写单元测试是个非常有挑战性,且难度不小的活,但 我依然推荐大家尝试去写一写单元测试,因为它所带来的好处不仅仅是大家想的那么简单:“只是 Bug 少了一点”。 所以,今天我会尝试从另外一些角度来讨论单测可以给我们带来哪些好处。

接着刚刚说到的 “只是 Bug 少一点” 这句话,可能大多数觉得单测就是在提测前减少一点 Bug 而已:

这样的想法确实是最直观的。但这只是想到了第一层,如果我们把 开发流程所有步骤 都加进来,会发现是这样的:

开发过程 后面,几乎每个流程都可能抛出 Bug。越是到后面流程才抛出的 Bug,程序员就越是要投入比开发阶段更大的时间和业务,而且所承受的风险也是最高的。

或许大家会想:不就改个 Bug,改几行而已。 可是大家有没有想过在跟测的过程中,很可能你已经开始另一个需求的评审了! 此时的你在解决突然插入的 Bug 的时候,心态还会像刚开始写代码时候那么轻松么?

实际上,还有更多的隐性成本没有考虑,比如反复确认产品逻辑、反复确认交互设计、反复确认前后端接口设计、各端对产品的理解。 有的时候,你就会发现这样很魔幻的场景:明明是一个字段的展示问题,竟然要花上一上午,拉了 4、5 个人来开会核对的情况。

下面这张图,也在说明两个问题:一是 85% 的缺陷都在代码设计阶段产生;二是发现 Bug 的阶段越靠后,耗费成本就越高,呈指数级别的增长。这种 “指数成本” 的案例也经常发生,当我们改正一个 Bug 的时候,可能随之而来又会多出 3 个 Bug,俗称:改崩了。

所以,在早期的单元测试就能发现bug,不仅可以省时省力,在开发流程上提高效率,也能降低反复修改出现的风险和时间成本。

如何做好单元测试?

如何做好单元测试?你首先必须弄清楚单元测试的对象是代码,以及代码的基本特征和产生错误的原因,然后你必须掌握单元测试的基本方法和主要技术手段。

代码基本特征与产生错误的原因

无论是开发语言还是脚本语言,都会有条件分支、循环处理和函数调用等最基本的逻辑控制,如果抛开代码需要实现的具体业务逻辑,仅看代码结构的话,你会发现所有的代码都是在对数据进行分类处理,每一次条件判定都是一次分类处理,嵌套的条件判定或者循环执行,也是在做分类处理。

可见,要做到代码功能逻辑正确,必须做到分类正确并且完备无遗漏,同时每个分类的处理逻辑必须正确。而在开发实践的过程中,通常考虑从以下方面考虑

  • 如果要实现正确的功能逻辑,会有哪几种正常的输入

  • 是否有需要特殊处理的多种边界输入

  • 各种潜在非法输入的可能性以及如何处理。

测试用例

单元测试的用例是一个输入数据预计输出的集合。

完整的输入数据

  • 被测试函数的输入参数

  • 被测试函数内部需要读取的全局静态变量

  • 被测试函数内部需要读取的成员变量

  • 函数内部调用子函数获得的数据

  • 函数内部调用子函数改写的数据

  • 嵌入式系统中,在中断调用时改写的数据

明确的预计输出

  • 被测试函数的返回值;

  • 被测试函数的输出参数

  • 被测试函数所改写的成员变量

  • 被测试函数所改写的全局变量

  • 被测试函数中进行的文件更新

  • 被测试函数中进行的数据库更新

  • 被测试函数中进行的消息队列更新

驱动代码,桩代码、Mock代码

驱动代码桩代码Mock代码,是单元测试中最常出现的三个名词

  • 驱动代码:指调用被测函数的代码,单元测试过程中,驱动模块通常包括调用被测函数前的数据准备、调用被测函数以及验证相关结果三个步骤。驱动代码的结构,通常由单元测试的框架决定。

  • 桩代码:是用来代替真实代码的临时代码。 比如,某个函数 A 的内部实现中调用了一个尚未实现的函数 B,为了对函数 A 的逻辑进行测试,那么就需要模拟一个函数 B,这个模拟的函数 B 的实现就是所谓的桩代码。

  • Mock:Mock 代码和桩代码非常类似,都是用来代替真实代码的临时代码,起到隔离和补齐的作用。在使用 Mock 代码的测试中,对于结果的验证(也就是 assert),通常出现在 Mock 函数中。

如何开展单元测试?

实际软件项目中开展单元测试

  • 并不是所有的代码都要进行单元测试,通常只有底层模块或者核心模块的测试中才会采用单元测试。

  • 你需要确定单元测试框架的选型,这和开发语言直接相关。

  • 为了能够衡量单元测试的代码覆盖率,通常你还需要引入计算代码覆盖率的工具。

  • 最后你需要把单元测试执行、代码覆盖率统计和持续集成流水线做集成,以确保每次代码递交,都会自动触发单元测试,并在单元测试执行过程中自动统计代码覆盖率,最后以“单元测试通过率”和“代码覆盖率”为标准来决定本次代码递交是否能够被接受。