在上一课《代码测试入门》中,我们讨论了如何编写并保存简单测试。本课将探讨应编写哪些类型的测试,以确保代码正确性。
代码覆盖率
“代码覆盖率”用于描述测试期间程序源代码中被执行到的比例。业内存在多种覆盖率度量指标,下文将介绍几种较为常用且有价值的指标。
语句覆盖率
语句覆盖率指测试例已执行的语句占全部语句的百分比。
考虑以下函数:
int foo(int x, int y)
{
int z{ y };
if (x > y)
{
z = x;
}
return z;
}
调用 foo(1, 0) 即可实现对该函数的 100% 语句覆盖,因为函数内所有语句都会被执行。
对于 isLowerVowel() 函数:
bool isLowerVowel(char c)
{
switch (c) // 语句 1
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true; // 语句 2
default:
return false; // 语句 3
}
}
由于一次调用无法同时覆盖语句 2 与语句 3,因此至少需要两次调用才能覆盖全部语句。
虽然追求 100% 语句覆盖率是良好目标,但仅凭此通常不足以确保程序正确。
分支覆盖率
分支覆盖率指已执行分支占总分支的百分比,其中每个可能分支均单独计数。一条 if 语句包含两个分支——条件为真时执行的分支,以及条件为假时执行的分支(即使无对应的 else 语句亦然)。switch 语句则可能包含多个分支。
int foo(int x, int y)
{
int z{ y };
if (x > y)
{
z = x;
}
return z;
}
此前的 foo(1, 0) 调用虽达到 100% 语句覆盖,且验证了 x > y 的场景,但仅覆盖了 50% 的分支。需再调用一次 foo(0, 1),以测试 if 语句不执行的分支。
bool isLowerVowel(char c)
{
switch (c)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true;
default:
return false;
}
}
对于 isLowerVowel() 函数,同样需要两次调用才能达成 100% 分支覆盖:一次(如 isLowerVowel(‘a’))测试前几个 case,另一次(如 isLowerVowel(‘q’))测试 default 分支。多个 case 共享同一代码体时,无需逐一测试——若其中一个通过,其余亦应通过。
再看以下函数:
void compare(int x, int y)
{
if (x > y)
std::cout << x << " is greater than " << y << '\n'; // 情况 1
else if (x < y)
std::cout << x << " is less than " << y << '\n'; // 情况 2
else
std::cout << x << " is equal to " << y << '\n'; // 情况 3
}
为达成 100% 分支覆盖,需三次调用:
- compare(1, 0) 测试第一个 if 的“真”分支;
- compare(0, 1) 测试第一个 if 的“假”分支以及第二个 if 的“真”分支;
- compare(0, 0) 测试前两个 if 的“假”分支并执行 else 分支。
因此,仅用三次调用即可认为该函数已得到可靠测试(远优于 18 百亿亿次)。
最佳实践
力求实现 100% 分支覆盖。
循环覆盖率
循环覆盖率(俗称 0、1、2 测试法)指出:若代码中存在循环,应确保其在迭代 0 次、1 次与 2 次时均能正确工作。若 2 次迭代正确,则迭代次数大于 2 时亦应正确。因而,这三类测试即可覆盖全部可能情形(循环不可能执行负数次)。
示例:
#include <iostream>
void spam(int timesToPrint)
{
for (int count{ 0 }; count < timesToPrint; ++count)
std::cout << "Spam! ";
}
为充分测试该循环,应调用三次:
- spam(0) 测试零次迭代;
- spam(1) 测试一次迭代;
- spam(2) 测试两次迭代。
若 spam(2) 正确,则对于所有 n > 2 的 spam(n) 亦应正确。
最佳实践
使用 0、1、2 测试法确保循环在不同迭代次数下均能正确工作。
测试不同类别的输入
编写接受参数的函数或接收用户输入时,应考虑各类输入的处理情况。此处“类别”指具有相似特征的一组输入。
例如,若编写求整数平方根的函数,应测试哪些值?首先可测试常规值,如 4;同时应测试 0 与负数。
以下为类别测试的基本准则:
整型:务必验证函数如何处理负值、零及正值。若涉及溢出,亦需检查。
浮点型:务必验证函数如何处理存在精度误差的值(略大或略小于预期)。推荐测试的 double 值包括 0.1 与 -0.1(略大),以及 0.7 与 -0.7(略小)。
字符串:务必验证函数如何处理空字符串、字母数字字符串、含空白字符(前导、尾部、内部)的字符串,以及全空白字符串。
若函数接收指针,切勿遗漏测试 nullptr(若尚未学习,可暂不深究)。
最佳实践
针对不同类别的输入值进行测试,以确保单元能正确处理。
测验
问题 1
什么是分支覆盖率?
显示答案
问题 2
以下函数最少需要多少次测试才能验证其正确性?
bool isLowerVowel(char c, bool yIsVowel)
{
switch (c)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true;
case 'y':
return yIsVowel;
default:
return false;
}
}