Skip to content

7.13 - 代码覆盖率

Key Takeaway

7.12 - 代码测试 中我们介绍了如何编写和保存测试。在这节课中,我们将探讨哪些测试对于保证程序质量是至关重要的。

代码覆盖率

术语代码覆盖率用于描述测试执行时可以运行的程序源代码数量。代码覆盖率有许多不同的度量标准。我们将在接下来的部分中介绍一些更有用和更受欢迎的方法。

语句覆盖率

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

考虑以下函数:

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语句'有两个分支——一个分支在条件为'真'时执行,另一个分支在条件为'假'时执行(即使没有相应的' 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')) 测试前面的几个case分支,第二次调用(例如 isLowerVowel('q')) 来测试default分支。能够在一次测试中执行到的分支,不需要分别测试。

考虑下面的函数:

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'; // 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%分支覆盖:compare(1, 0) 测试第一个if语句为真的分支,compare(0, 1) 测试第一个if语句为假且第二个if语句为真的分支。compare(0, 0) 测试两个if语句都为假,执行else的分支。执行完这三次测试后,我们就有信心说这个函数被可靠地测试了(三次测试总好过1800亿亿个输入组合吧)。

最佳实践

100%分支覆盖率是我们的测试目标!

循环覆盖率

循环覆盖率(非正式地称为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) 测试0次迭代的情况,spam(1) 测试1次迭代的情况,然后调用 spam(2) 测试 2 次迭代的情况。如果 spam(2) 可以工作,则对于任意 n>2 的情况 spam(n) 都应该可以工作。

最佳实践

使用 “0,1,2测试” 来确保你的循环在不同次数的迭代下都能正常工作。

测试不同类别的输入

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

例如,对于一个产生整数平方根的函数,用什么值来测试它是有意义的?你可能会从一些正常的值开始,比如“4”。但是用“0”和负数来测试也是一个好主意。

以下是分类测试的一些基本准则:

对于整数,需要考虑函数如何处理负值、零和正值。如果可能的话,还应该检查是否溢出。

对于浮点数,确保考虑了函数如何处理有精度问题的值(略大于或略小于预期值)。适合测试的double类型值是 0.1-0.1 (用于测试略大于预期的数字)以及 0.6-0.6 (用于测试略小于预期的数字)。

对于字符串,确保你考虑了函数如何处理空字符串(只是一个空结束符)、正常有效的字符串、有空格的字符串以及全是空格的字符串。

如果函数会处理指针,不要忘记测试 nullptr (如果你不知道这是什么也不要紧,毕竟我们还没介绍过它)。

最佳实践

测试不同类别的输入值,以确保代码单元能够正确地处理它们。