9-2 代码覆盖率
在上一课9.1——代码测试介绍中,我们讨论了如何编写并维护简单的测试。本课将探讨为确保代码正确性而编写哪些类型的测试最为有效。
代码覆盖率
代码覆盖率code coverage用于描述程序源代码在测试过程中被执行的比例。代码覆盖率有多种不同的衡量指标,以下章节将介绍其中一些实用且常用的指标。
语句覆盖率
语句覆盖率statement coverage指代码中被测试例程执行的语句所占的比例。
考虑以下函数:
int foo(int x, int y)
{
int z{ y };
if (x > y)
{
z = x;
}
return z;
}
调用此函数时,若以 foo(1, 0) 的形式调用,将实现该函数的完整语句覆盖率,因为函数中的每条语句都会被执行。
对于我们的 isLowerVowel() 函数:
bool isLowerVowel(char c)
{
switch (c) // statement 1
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true; // statement 2
default:
return false; // statement 3
}
}
该函数需要调用两次才能测试所有语句,因为无法通过单次函数调用同时触发语句2和语句3。
虽然追求100%语句覆盖率是好的,但这通常不足以确保正确性。
分支覆盖率
分支覆盖率branch coverage指已执行的分支所占的百分比,每个可能的分支均单独计数。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’))用于测试前几个分支,另一次(例如 isLowerVowel(‘q’))用于测试默认分支。多个指向相同主体的分支无需单独测试——若其中一个能正常工作,其余分支也应正常工作。
现在考虑以下函数:
void compare(int x, int y)
{
if (x > y)
std::cout << x << " is greater than " << y << '\n'; // case 1
else if (x < y)
std::cout << x << " is less than " << y << '\n'; // case 2
else
std::cout << x << " is equal to " << y << '\n'; // case 3
}
要实现100%分支覆盖率需要3次调用:compare(1, 0)测试了第一个if语句的正向用例;compare(0, 1)测试了第一个if语句的负向用例和第二个if语句的正向用例;compare(0, 0)测试了第一个和第二个if语句的负向用例,并执行else语句。因此,我们可认定该函数通过3次调用即可实现可靠测试(其可靠性略优于18京次调用)。
最佳实践
力求实现代码的100%分支覆盖率。
循环覆盖率
循环覆盖率loop coverage(非正式称为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) 能正常运行,则 spam(n) 亦应有效,其中 n > 2。
最佳实践
使用0、1、2测试来确保您的循环在不同迭代次数下都能正常工作。
测试不同类别的输入
在编写接受形参的函数或接收用户输入时,需考虑不同类别输入的情况。此处“类别”指具有相似特征的输入集合。
例如,若编写一个计算整数平方根的函数,应测试哪些数值?通常会从常规值(如4)开始测试,但同时应测试0和负数的情况。
以下是类别测试的基本准则:
整数类:确保函数能正确处理负数、零值和正数,必要时需检查溢出问题。
浮点数方面:需验证函数对精度偏差值(实际值略大于/小于预期值)的处理方式。推荐测试双精度数值: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;
}
}
显示方案
4个测试用例。一个用于测试a/e/i/o/u的情况。一个用于测试默认情况。一个用于测试isLowerVowel(‘y’, true)。还有一个用于测试isLowerVowel(‘y’, false)。

浙公网安备 33010602011771号