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)。
posted @ 2026-03-01 10:23  游翔  阅读(1)  评论(0)    收藏  举报