9.2 — 代码覆盖率

在上一课 9.1 — 代码测试简介 中,我们讨论了如何编写和保存简单测试。在本课中,我们将讨论编写哪些类型的测试对于确保代码正确性很有用。

代码覆盖率

术语代码覆盖率用于描述程序源代码在测试过程中执行了多少。代码覆盖率有许多不同的度量标准。我们将在以下部分介绍一些更实用和更流行的度量标准。

语句覆盖率

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

考虑以下函数

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% 的语句覆盖率是好的,但它通常不足以确保正确性。

分支覆盖率

分支覆盖率指已执行的分支的百分比,每个可能的分支单独计数。一个 if 语句有两个分支——当条件为 true 时执行的分支,以及当条件为 false 时执行的分支(即使没有相应的 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
}

这里需要 3 次调用才能获得 100% 的分支覆盖率:compare(1, 0) 测试第一个 if 语句的肯定用例。compare(0, 1) 测试第一个 if 语句的否定用例和第二个 if 语句的肯定用例。compare(0, 0) 测试第一个和第二个 if 语句的否定用例并执行 else 语句。因此,我们可以说此函数通过 3 次调用(比 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) 有效,那么 spam(n) 应该有效,其中 n > 2

最佳实践

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

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
69 条评论
最新
最早 最多投票
内联反馈
查看所有评论