在上一课 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
(如果这没有意义,请不要担心,我们尚未涉及它)。
最佳实践
测试不同类别的输入值以确保您的单元正确处理它们。
小测验时间
问题 #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;
}
}