代码覆盖率

在上一课《代码测试入门》中,我们讨论了如何编写并保存简单测试。本课将探讨应编写哪些类型的测试,以确保代码正确性。

代码覆盖率

“代码覆盖率”用于描述测试期间程序源代码中被执行到的比例。业内存在多种覆盖率度量指标,下文将介绍几种较为常用且有价值的指标。

语句覆盖率

语句覆盖率指测试例已执行的语句占全部语句的百分比。

考虑以下函数:

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;
    }
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"