即使我有 100% 的代码覆盖率,我的代码仍可能包含哪些类型的错误?我正在寻找具体示例或此类错误的具体示例的链接。
21 回答
拥有 100% 的代码覆盖率并不像人们想象的那么好。考虑一个简单的例子:
double Foo(double a, double b)
{
return a / b;
}
即使是单个单元测试也会将此方法的代码覆盖率提高到 100%,但所述单元测试不会告诉我们哪些代码在工作,哪些代码不工作。这可能是一个完全有效的代码,但没有测试边缘条件(例如 when b
is 0.0
),单元测试充其量是不确定的。
代码覆盖率只告诉我们单元测试执行了什么,而不是它是否正确执行。这是一个重要的区别。仅仅因为单元测试执行了一行代码,并不一定意味着该行代码按预期工作。
听听这个有趣的讨论。
代码覆盖率并不意味着您的代码在任何方面都没有错误。这是对测试用例覆盖源代码库的程度的估计。100% 的代码覆盖率意味着每一行代码都经过测试,但程序的每个状态肯定不是。在这方面正在进行研究,我认为它被称为有限状态建模,但它确实是一种尝试探索程序每个状态的蛮力方式。
做同样事情的一种更优雅的方式被称为抽象解释。MSR(微软研究院)发布了基于抽象解释的名为CodeContracts的东西。还请查看Pex,他们确实强调了测试应用程序运行时行为的切割方法。
我可以编写一个非常好的测试,它会给我很好的覆盖率,但不能保证该测试会探索我的程序可能具有的所有状态。这是编写非常好的测试的问题,这很难。
代码覆盖率并不意味着好的测试
呃?任何一种普通的逻辑错误,我猜?内存损坏、缓冲区溢出、普通的旧错误代码、赋值而不是测试,不胜枚举。覆盖范围仅此而已,它让您知道所有代码路径都已执行,而不是它们是否正确。
正如我还没有看到它提到的那样,我想添加这个线程,代码覆盖率不会告诉你代码的哪一部分是没有错误的。
它只告诉您代码的哪些部分保证未经测试。
1.“数据空间”问题
你的(坏)代码:
void f(int n, int increment)
{
while(n < 500)
{
cout << n;
n += increment;
}
}
你的测试:
f(200,100);
实际使用中的错误:
f(200,0);
我的观点:您的测试可能覆盖 100% 的代码行,但它不会(通常)覆盖所有可能的输入数据空间,即所有可能的输入值的集合。
2. 测试你自己的错误
另一个经典的例子是当你在设计中做了一个错误的决定,并根据你自己的错误决定测试你的代码。
例如,规范文档说“打印所有素数到n ”并且你打印所有素数到n但不包括 n。你的单元测试会测试你的错误想法。
3. 未定义的行为
使用未初始化变量的值,导致无效的内存访问等,并且您的代码具有未定义的行为(在 C++ 或任何其他考虑“未定义行为”的语言中)。有时它会通过你的测试,但它会在现实世界中崩溃。
...
总是会出现运行时异常:内存已满、数据库或其他连接未关闭等...
考虑以下代码:
int add(int a, int b)
{
return a + b;
}
此代码可能无法实现一些必要的功能(即不满足最终用户的要求):“100% 覆盖率”不一定测试/检测应该实现但没有实现的功能。
此代码适用于某些但不是所有输入数据范围(例如,当 a 和 b 都非常大时)。
如果您的测试包含错误,或者您正在测试错误的东西,代码覆盖率没有任何意义。
作为相关切线;我想提醒你,我可以简单地构造一个满足以下伪代码测试的 O(1) 方法:
sorted = sort(2,1,6,4,3,1,6,2);
for element in sorted {
if (is_defined(previousElement)) {
assert(element >= previousElement);
}
previousElement = element;
}
Jon Skeet 的额外业力,他指出了我正在考虑的漏洞
代码覆盖率通常只告诉你一个函数中有多少分支被覆盖。它通常不会报告函数调用之间可以采用的各种路径。程序中的许多错误发生是因为从一种方法到另一种方法的切换是错误的,而不是因为方法本身包含错误。这种形式的所有错误仍然可能存在于 100% 的代码覆盖率中。
在我的机器上工作
许多事情在本地机器上运行良好,我们不能保证在暂存/生产上运行。代码覆盖率可能不会涵盖这一点。
测试中的错误:)
好吧,如果您的测试没有测试涵盖的代码中发生的事情。如果您有此方法可以向属性添加数字,例如:
public void AddTo(int i)
{
NumberA += i;
NumberB -= i;
}
如果您的测试只检查 NumberA 属性,而不检查 NumberB,那么您将有 100% 的覆盖率,测试通过,但 NumberB 仍将包含错误。
结论:100% 的单元测试并不能保证代码没有错误。
参数验证,又名。空检查。如果您接受任何外部输入并将它们传递给函数但从不检查它们是否有效/为空,那么您可以实现 100% 的覆盖率,但是如果您以某种方式将 null 传递给函数,您仍然会得到 NullReferenceException,因为这是您的数据库给出的你。
还有,算术溢出,比如
int result = int.MAXVALUE + int.MAXVALUE;
代码覆盖率仅涵盖现有代码,它无法指出您应该在哪里添加更多代码。
我不知道其他人,但我们没有接近 100% 的覆盖率。我们的“这不应该发生”的 CATCH 都没有在我们的测试中得到锻炼(嗯,有时他们会这样做,但随后代码会被修复,所以他们不会再发生了!)。恐怕我不担心在从未发生过的 CATCH 中可能存在语法/逻辑错误
您的产品可能在技术上是正确的,但不能满足客户的需求。
仅供参考,Microsoft Pex 试图通过探索您的代码并找到“边缘”情况来提供帮助,例如除以零、溢出等。
该工具是 VS2010 的一部分,但您可以在 VS2008 中安装技术预览版。该工具可以找到它找到的东西,这是非常了不起的,但是,IME,它仍然不会让你一路“防弹”。
执行 100% 的代码覆盖率,即 100% 的指令、100% 的输入和输出域、100% 的路径、100% 的任何你想到的,你的代码中仍然可能存在错误:缺少功能。
代码覆盖率意义不大。重要的是是否涵盖了所有(或大部分)影响行为的参数值。
例如,考虑一个典型的 compareTo 方法(在 java 中,但适用于大多数语言):
//Return Negative, 0 or positive depending on x is <, = or > y
int compareTo(int x, int y) {
return x-y;
}
只要您对 进行了测试compareTo(0,0)
,就可以获得代码覆盖率。但是,您在这里至少需要 3 个测试用例(用于 3 个结果)。它仍然不是没有错误的。添加测试以涵盖异常/错误情况也是值得的。在上述情况下,如果你尝试compareTo(10, Integer.MAX_INT)
,它将失败。
底线:尝试根据行为将输入划分为不相交的集合,对每个集合中的至少一个输入进行测试。这将增加真正意义上的覆盖范围。
还要检查诸如QuickCheck 之类的工具(如果适用于您的语言)。
正如这里的许多答案所提到的,您可能拥有 100% 的代码覆盖率并且仍然存在错误。
最重要的是,您可能有 0 个错误,但代码中的逻辑可能不正确(不符合要求)。代码覆盖率或 100% 无错误根本无法帮助您。
典型的企业软件开发实践如下:
- 有一个清晰的功能规范
- 制定针对 (1) 的测试计划并进行同行评审
- 针对 (2) 编写测试用例并进行同行评审
- 根据功能规范编写代码并进行同行评审
- 根据测试用例测试您的代码
- 做代码覆盖率分析,编写更多的测试用例,达到良好的覆盖率。
请注意,我说的是“好”而不是“100%”。100% 的覆盖率可能并不总是可行的——在这种情况下,最好将精力花在实现代码的正确性上,而不是覆盖一些晦涩的分支。在上面的第 1 步到第 5 步中,不同类型的事情都可能出错:错误的想法、错误的规范、错误的测试、错误的代码、错误的测试执行……最重要的是,仅第 6 步并不是最重要的一步。过程。
没有任何错误且覆盖率为 100% 的错误代码的具体示例:
/**
* Returns the duration in milliseconds
*/
int getDuration() {
return end - start;
}
// test:
start = 0;
end = 1;
assertEquals(1, getDuration()); // yay!
// but the requirement was:
// The interface should have a method for returning the duration in *seconds*.
几乎所有东西。
你读过完整的代码吗?(因为 StackOverflow 说你真的应该这样做。)在第 22 章中它说“100% 的语句覆盖率是一个好的开始,但这还不够”。本章的其余部分解释了如何确定要添加哪些附加测试。这是一个简短的品尝者。
结构化基础测试和数据流测试意味着通过程序测试每个逻辑路径。下面的人为代码有四个路径,具体取决于 A 和 B 的值。100% 的语句覆盖率可以通过仅测试四个路径中的两个来实现,也许
f=1:g=1/f
和f=0:g=f+1
。但f=0:g=1/f
会给出除以零的错误。您必须考虑if语句以及while和for循环(循环体可能永远不会被执行)以及select或switch语句的每个分支。If A Then
f = 1
Else
f = 0
End If
If B Then
g = f + 1
Else
g = f / 0
End If
错误猜测- 对经常导致错误的输入类型进行有根据的猜测。例如边界条件(关闭一个错误)、无效数据、非常大的值、非常小的值、零、空值、空集合。
即便如此,您的要求中可能存在错误,测试中的错误等 - 正如其他人所提到的。