章节目录

代码覆盖率

本节阅读量:

上一课中,我们讨论了如何编写和保存简单测试。本课将讨论编写哪些类型的测试有助于确保代码正确。


代码覆盖率

术语代码覆盖率用于描述测试时执行了多少程序源代码。代码覆盖率有许多不同的衡量方式。在下面的部分中,我们将介绍几个有用且常见的方法。


语句覆盖率

术语语句覆盖率是指代码中被测试例程执行过的语句所占的百分比。

考虑以下函数:

1
2
3
4
5
6
7
8
9
int foo(int x, int y)
{
    int z{ y };
    if (x > y)
    {
        z = x;
    }
    return z;
}

调用foo(1, 0)将为该函数提供完整的语句覆盖,因为函数中的每条语句都会被执行。

对于isLowerVowel()函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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语句有两个分支——一个分支在条件为true时执行,另一个分支在条件为false时执行(即使没有对应的else语句要执行)。switch语句可以有多个分支。

1
2
3
4
5
6
7
8
9
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语句不执行的用例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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’))用于测试default情况。多个case标签对应同一组执行语句时,只需要为其中一个case标签编写测试用例即可——如果这个用例能正常工作,其他并列的case标签通常也可以。

现在考虑以下函数:

1
2
3
4
5
6
7
8
9
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
}

这里需要3次调用才能获得100%的分支覆盖率:compare(1, 0)测试第一个if语句的肯定用例;compare(0, 1)测试第一个if语句的否定用例和第二个if语句的肯定用例;compare(0, 0)测试第一个和第二个if语句的否定用例,并执行else语句。因此,可以说该函数通过3次调用得到了可靠测试(略优于1.8千亿亿次)。


循环覆盖率

循环覆盖率(非正式地称为0、1、2测试)表示,如果代码中有循环,则应确保它在迭代0次、1次和2次时都能正常工作。如果它在2次迭代时正确工作,那么它通常也应该能在大于2次的所有迭代中正确工作。因此,这三个测试覆盖了所有关键可能性(因为循环不能执行负次数)。

考虑以下程序:

1
2
3
4
5
6
7
#include <iostream>

void spam(int timesToPrint)
{
    for (int count{ 0 }; count < timesToPrint; ++count)
         std::cout << "Spam! ";
}

要在该函数中正确测试循环,应该调用它三次:spam(0)测试零次迭代用例,spam(1)测试一次迭代用例,spam(2)测试两次迭代用例。如果参数2有效,则参数n通常也应该有效,其中n > 2。


测试不同类别的输入

在编写接受参数的函数时,或者在接受用户输入时,需要考虑不同类别的输入会发生什么。在这种情况下,我们使用术语“类别”来表示具有类似特征的一组输入。

例如,如果我编写了一个用于计算整数平方根的函数,那么用哪些值来测试它比较有意义?您可能会从一些正常值开始,例如4。但使用0和负数进行测试也是一个好主意。

下面是类别测试的一些基本准则:

对于整数,请确保考虑函数如何处理负值、零值和正值。如果可能,还应该检查溢出。

对于浮点数,请确保考虑函数如何处理存在精度问题的值(比预期稍大或稍小的值)。用于测试的double值可以选择0.1和-0.1(用于测试比预期稍大的数字,如果预期是0.000x),以及0.6和-0.6(用于测试略小于预期的数字,如果预期是xxxxx.yy)。

对于字符串,请确保考虑函数如何处理空字符串、含字母和数字的字符串、包含空格的字符串(前导、尾随和内部空格),以及全部为空格的字符串。

如果函数采用指针,也不要忘记测试nullptr(将在后续介绍)。


9.0 代码测试简介

上一节

9.2 常见的语义错误

下一节