如果测试了程序的每条路径,是否可以保证找到所有错误?
如果没有,为什么不呢?如何解决程序流程的每一种可能的组合,如果存在的话却找不到问题?
我毫不犹豫地建议可以找到“所有错误”,但这也许是因为路径覆盖不切实际(因为它是组合的),所以从未经历过?
注意:本文提供了我考虑的覆盖类型的快速摘要。
如果测试了程序的每条路径,是否可以保证找到所有错误?
如果没有,为什么不呢?如何解决程序流程的每一种可能的组合,如果存在的话却找不到问题?
我毫不犹豫地建议可以找到“所有错误”,但这也许是因为路径覆盖不切实际(因为它是组合的),所以从未经历过?
注意:本文提供了我考虑的覆盖类型的快速摘要。
Answers:
如果测试了程序的每条路径,是否可以保证找到所有错误?
没有
如果没有,为什么不呢?如何解决程序流程的每一种可能的组合,如果存在的话却找不到问题?
因为即使测试了所有可能的路径,仍然没有用所有可能的值或所有可能的值组合测试它们。例如(伪代码):
def Add(x as Int32, y as Int32) as Int32:
return x + y
Test.Assert(Add(2, 2) == 4) //100% test coverage
Add(MAXINT, 5) //Throws an exception, despite 100% test coverage
自从有人指出程序测试可以令人信服地表明存在错误,但是永远不能证明它们不存在以来,已经过去了二十年。在认真引用了这一广为人知的言论后,软件工程师回到了日常工作中,继续完善他的测试策略,就像昔日的炼金术士一样,继续完善他的金相纯化。
- EW Dijkstra算法(着重写于1988年,它已经相当现在已经超过二十年。)
除了梅森的答案之外,还有另一个问题:覆盖率不能告诉您测试了什么代码,而是告诉您执行了什么代码。
假设您有一个测试套件,其路径覆盖率为100%。现在删除所有断言,然后再次运行测试套件。测试者Voilà仍然具有100%的路径覆盖率,但它绝对不会测试任何东西。
ON ERROR GOTO
也是C的路径if(errno)
。
考虑该abs
函数,该函数返回数字的绝对值。这是一个测试(Python,设想一些测试框架):
def test_abs_of_neg_number_returns_positive():
assert abs(-3) == 3
此实现是正确的,但仅获得60%的代码覆盖率:
def abs(x):
if x < 0:
return -x
else:
return x
这个实现是错误的,但是它获得了100%的代码覆盖率:
def abs(x):
return -x
def abs(x): if x == -3: return 3 else: return 0
您可能会抽出else: return 0
零件并获得100%的覆盖率,但是即使该功能确实通过了单元测试,该函数实际上也没有用。
Mason的回答的另一个补充是,程序的行为可能取决于运行时环境。
以下代码包含“售后使用”:
int main(void)
{
int* a = malloc(sizeof(a));
int* b = a;
*a = 0;
free(a);
*b = 12; /* UAF */
return 0;
}
此代码是“未定义行为”,具体取决于配置(版本|调试),操作系统和编译器,它将产生不同的行为。不仅路径覆盖不能保证您会找到UAF,而且测试套件通常也不会覆盖取决于配置的UAF的各种可能行为。
另一个要注意的是,即使路径覆盖可以确保找到所有错误,但实际上不可能在任何程序上都可以实现。请考虑以下内容:
int main(int a, int b)
{
if (a != b) {
if (cryptohash(a) == cryptohash(b)) {
return ERROR;
}
}
return 0;
}
如果您的测试人员可以为此生成所有路径,那么恭喜您是密码学家。
cryptohash
,很难说“足够小”是什么。在超级计算器上可能需要两天才能完成。但是,是的,int
可能会有点小short
。
从其他答案中可以很明显地看出,测试中100%的代码覆盖率并不意味着100%的代码正确性,甚至并不意味着所有可以通过测试发现的错误都将被发现(不要介意没有任何测试可以捕获的错误)。
回答此问题的另一种方法是实践中的一种方法:
在现实世界中,实际上在您自己的计算机上,有许多软件是使用一组可以提供100%覆盖率的测试开发的,但是仍然存在错误,包括可以更好地测试的错误。
因此,一个必然的问题是:
代码覆盖率工具的重点是什么?
代码覆盖率工具可帮助您确定一个被忽略测试的区域。可能很好(即使未经测试,代码也正确无误),可能无法解决(由于某种原因,无法找到路径),或者可能是现在或以后进行修改的臭臭虫的所在地。
在某些方面,拼写检查是可比较的:某些东西可以“通过”拼写检查,并且会以与词典中的单词匹配的方式被拼错。否则它可能会“失败”,因为词典中没有正确的单词。否则,它可以通过并完全是胡说八道。拼写检查是一种工具,可以帮助您确定在校对过程中可能错过的地方,但正如它不能保证进行完整正确的校对一样,因此代码覆盖范围也不能保证进行完整正确的测试。
当然,错误地使用拼写检查的方法是众所周知的,它伴随着每条建议母羊的建议,因此,如果母羊把钱借给它,那么回避就变得更糟了。
使用代码覆盖率可能会很诱人,尤其是如果您有近乎完美的98%的情况下,填写案例以使剩余的路径被点击。
这等同于使用拼写检查进行纠正,即所有单词都是风雨无阻,或者都是所有合适的单词。结果就是一团糟。
但是,如果考虑未覆盖路径真正需要的测试,则代码覆盖工具将完成其工作。不是保证您的正确性,而是指出了一些需要完成的工作。
路径覆盖无法告诉您是否已实现所有必需的功能。遗漏某个功能是一个错误,但是路径覆盖不会检测到它。
问题的部分原因是100%的覆盖率仅保证代码在一次执行后即可正常运行。诸如内存泄漏之类的某些错误可能在一次执行后就不明显或不会引起问题,但是随着时间的流逝会给应用程序造成问题。
例如,假设您有一个连接到数据库的应用程序。也许用一种方法,程序员在完成查询后会忘记关闭与数据库的连接。您可以对该方法进行几次测试,但不会发现其功能有任何错误,但是您的数据库服务器可能会遇到无法连接的情况,因为该特定方法完成后并没有关闭连接,打开的连接必须现在超时。
times_two(x) = x + 2
,则测试套件将完全覆盖它assert(times_two(2) == 4)
,但这显然仍然是错误的代码!不需要内存泄漏:)
其他答案很好,但我只想补充一下,“测试程序的每条路径”的条件本身都是模糊的。
考虑以下方法:
def add(num1, num2)
foo = "bar" # useless statement
$global += 1 # side effect
num1 + num2 # actual work
end
如果您编写一个断言的测试,则add(1, 2) == 3
代码覆盖率工具将告诉您每一行都在执行。但是您实际上尚未断言有关全局副作用或无用分配的任何信息。这些行已执行,但尚未经过实际测试。
变异测试将有助于发现此类问题。突变测试工具将提供一系列“突变”代码并查看测试是否仍通过的预定方法。例如:
+=
为-=
。该突变不会导致测试失败,因此可以证明您的测试对全局副作用没有任何意义。本质上,变异测试是测试您的测试的一种方法。但是就像您永远不会使用所有可能的输入集来测试实际功能一样,您也永远不会运行每种可能的变异,因此同样,这是有限的。
我们可以做的每个测试都是向无错误程序迈进的启发。没有什么是完美的。
好吧... 是的,实际上,如果“通过”程序的每条路径都经过测试。但这意味着,贯穿程序可能具有的所有可能状态的整个空间的所有可能路径,包括所有变量。即使对于非常简单的静态编译程序(例如,旧的Fortran数字处理程序),这也不可行,尽管至少可以想象得到:如果只有两个整数变量,则基本上是在处理连接点的所有可能方式。二维网格;实际上看起来很像旅行推销员。对于n个这样的变量,您要处理的是n维空间,因此对于任何实际程序而言,任务是完全难以处理的。
更糟的是:严重的东西,你不只是原始变量固定数量,但在函数调用动态创建变量,或在图灵完备的语言具有可变大小的变量...之类的东西,尽可能。这就给状态空间带来了无限的空间,即使拥有了非常强大的测试设备,也打破了所有覆盖范围的希望。
就是说...实际上情况并不那么暗淡。这是可能的坡口整个计划是正确的,但你必须放弃一些想法。
首先:强烈建议改用声明性的languange。出于某种原因,命令式语言一直以来都是最受欢迎的语言,但是它们将算法与实际交互结合在一起的方式使说“正确”的含义变得极其困难。
在纯函数式编程语言中,这要容易得多:它们在数学函数的真正有趣特性和您无法真正说出的模糊现实世界交互之间有着明显的区别。对于函数,很容易指定“正确的行为”:如果对于所有可能的输入(从参数类型)得出相应的期望结果,则函数的行为正确。
现在,您说这仍然很棘手...毕竟,所有可能参数的空间通常也是无限维的。没错–尽管对于单个功能,即使是简单的覆盖率测试也可以使您比命令式程序所希望的还要遥遥领先!但是,有一个不可思议的强大工具可以改变游戏规则:通用量化/ 参数多态性。基本上,这使您可以对非常通用的数据类型编写函数,并确保如果它仅适用于简单的数据示例,则它将完全适用于任何可能的输入。
至少在理论上。要找到真正通用的正确类型并不容易,您可以完全证明这一点–通常,您需要一种依赖类型的语言,而且这些语言往往很难使用。但是仅凭参数多态性以功能风格进行编写已经极大地提高了您的“安全级别”-您不一定会发现所有错误,但必须将它们很好地隐藏起来,以便编译器不会发现它们!