单元测试规范
目的
- 单元测试是编写测试代码,用以检测特定的、明确的、细颗粒的功能!
- 单元测试只针对功能点进行测试,不包括对业务流程正确性的测试。
测什么
- 接口功能性测试:
- 接口功能的正确性,即保证接口能够被正常调用,并输出有效数据。
- 边界条件测试。
- 所有独立代码测试:
- 保证每一句代码,所有分支都测试完成。
- 执行覆盖率:每个语句都执行到了。
- 分支覆盖率:每个分支都执行到了。
测试原则
- 遵循测试FIRST原则。
- F(Fast):测试要能快速运行。
- I(Isolate):测试用例要独立,不能相互依赖。
- R(Repeatable):测试要可以重复运行。
- S(Self-verifying):测试会自己检查产出。
- T(Timely):测试要及时做,与写代码紧密相连。
测试目标
- 执行覆盖率:>=90%。
- 分支覆盖率:100%。
- 函数覆盖率:100%。
测试单元
- 以函数作为最小测试单元。
编写以及维护人员
- 单元测试代码由该模块的开发者编写以及维护。
设计准则
1. 保持单元测试小巧,快速
- 理论上,任何代码提交前都应该完整跑一遍所有测试套件。保持测试代码执行迅捷能够缩短迭代开发周期。
2. 让单元测试很容易跑起来
- 对开发环境进行配置,最好是敲条命令或是点个按钮就能把单个测试用例或测试套件跑起来。
3. 对测试进行评估
- 对执行的测试进行覆盖率分析,得到精确的代码执行覆盖率,并调查哪些代码未被执行。
4. 立即修正失败的测试
- 每个开发人员在提交前都应该保证新的测试用例执行成功,当有代码提交时,现有测试用例也都能跑通。
- 如果一个定期执行的测试用例执行失败,整个团队应该放下手上的工作优先解决这个问题。
5. 保持测试的独立性
- 为了保证测试稳定可靠且便于维护,测试用例之间决不能有相互依赖,也不能依赖执行的先后次序。
6. 合理的命名测试用例
- 确保每个方法只测试
被测函数
的一个明确特性,并相应的命名测试方法。 - 典型的命名俗定是
test[what]
,比如TestSave()
,TestAddListener()
,TestDeleteProperty()
等。
7. 测试全部全局可见的函数接口
8. 看成是黑盒
- 站在第三方使用者的角度,测试一个函数是否满足规定的需求. 并设法让它出问题。
9. 看成是白盒
- 被测试函数是程序员自写自测的,应该在最复杂的逻辑部分多花些精力测试。
10. 先关注执行覆盖率
-
区别对待
执行覆盖率
和实际测试覆盖率
。 -
测试的最初目标应该是确保较高的执行覆盖率. 这样能保证代码在
少量
参数值输入时能执行成功. 一旦执行覆盖率就绪,就应该开始改进测试覆盖率了。 -
注意,实际的测试覆盖率很难衡量 (而且往往趋近于 0%)。
-
思考以下公有方法:
int SetLength(double length);
-
调用
SetLength(1.0)
你可能会得到 100% 的执行覆盖率。但要达到 100% 的实际测试覆盖率,有多少个double
浮点数这个方法就必须被调用多少次,并且要一一验证行为的正确性. 这无疑是不可能的任务。
11. 提供一个随机值生成器
- 当边界值都覆盖了,另一个能进一步改善测试覆盖率的简单方法就是生成随机参数,这样每次执行测试都会有不同的输入。
12. 每个特性只测一次
- 在测试模式下,有时会情不自禁的滥用断言。这种做法会导致维护更困难,需要极力避免. 仅对测试方法名指示的特性进行明确测试。
13. 使用显式断言
- 应该总是优先使用
AssertEquals(a, b)
而不是AssertTrue(a == b)
,因为前者会给出更有意义的测试失败信息. 在事先不确定输入值的情况下,这条规则尤为重要,比如之前使用随机参数值组合的例子。
14. 覆盖边界值
- 确保参数边界值均被覆盖. 对于数字,测试负数,0,正数,最小值,最大值,最小值+1,最大值-1,溢出值,NaN(非数字),无穷大等。
- 对于字符串,测试空字符串,单字符,非 ASCII 字符串,多字节字符串等。
- 对于集合类型,测试空,1,第一个,最后一个等。
- 对于日期,测试 1月1号,2月29号,12月31号等。
- 被测试的函数本身也会暗示一些特定情况下的边界值。
- 要点是尽可能彻底的测试这些边界值,因为它们都是主要
疑犯
。
15. 正确的输入,并得到预期的结果
16. 与设计文档相结合,来编写单元测试
17. 提供反向测试
-
反向测试是指刻意编写问题代码(如:非法数据、异常流程、非业务允许输入等),来验证鲁棒性和能否正确的处理错误。
-
假设如下方法的参数如果传进去的是负数,会返回-1:
int SetLength(double length) {return -1;}
-
可以用下面的方法来测试这个特例是否被正确处理:
AssertEquals(SetLength(-1), -1)
18. 保持你的测试是幂等的
- 你应该能够运行你的测试多次而不改变它的输出结果,并且测试也不应该改变任何的数据或者添加任何东西。无论是运行一次还是一百万次,它的效果都应该是一样的。
19. 代码设计时谨记测试
- 编写和维护单元测试的代价是很高的。
- 减少代码中的公有接口和循环复杂度是降低成本,使高覆盖率测试代码更易于编写和维护的有效方法。
- 一些建议:
- 避免不必要的逻辑分支。
- 在逻辑分支中编写尽可能少的代码。
- 在公有和私有接口中尽量多用异常和断言验证参数参数的有效性。
20. 权衡测试成本
- 不写单元测试的代价很高,但是写单元测试的代价同样很高。要在这两者之间做适当的权衡,如果用执行覆盖率来衡量,业界标准通常在
80%
左右。 - 很典型的,读写外部资源的错误处理和异常处理就很难达到百分百的执行覆盖率。
21. 安排测试优先次序
- 单元测试是典型的自底向上过程,如果没有足够的资源测试一个系统的所有模块,就应该先把重点放在较底层的模块。
22. 写测试用例重现 bug
- 每上报一个
bug
,都要写一个测试用例
来重现这个bug
(即无法通过测试),并用它作为成功修正代码的检验标准。
23. 了解局限
单元测试永远无法证明代码的正确性!!
- 一个跑失败的测试可能表明代码有错误,但一个跑成功的测试什么也证明不了。
结构规范
目录结构
-
源文件处于独立的文件夹内,则在该文件夹同级目录创建
test
文件夹存放测试相关文件。- module_x - src - include - test
-
源文件不处于独立的文件夹内,则在源文件所在目录直接存放测试相关文件。
- module_x - xx.c - xx.h - xx.test.c
文件命名
- 测试文件名与源文件名同名,后缀以
.test.文件类型
结尾。 - 每一个源文件对应一个同名的测试文件。