路径覆盖范围是否可以保证找到所有错误?


64

如果测试了程序的每条路径,是否可以保证找到所有错误?

如果没有,为什么不呢?如何解决程序流程的每一种可能的组合,如果存在的话却找不到问题?

我毫不犹豫地建议可以找到“所有错误”,但这也许是因为路径覆盖不切实际(因为它是组合的),所以从未经历过?

注意:本文提供了我考虑的覆盖类型的快速摘要。


33
这相当于停顿问题

31
如果应该存在的代码不存在怎么办?
RemcoGerlich 2015年

6
@Snowman:不,不是。无法解决所有程序的暂停问题,但对于许多特定程序而言,它是可以解决的。对于这些程序,可以在有限(尽管可能很长)的时间内枚举所有代码路径。
约根·福(JørgenFogh)

3
@JørgenFogh但是,当尝试在任何程序中查找错误时,程序是否停止是否不是先验的未知?这不是关于“通过路径覆盖查找任何程序中的所有错误” 的一般方法的问题吗?在这种情况下,这是否类似于“确定是否有任何程序停止”?
Andres F.

1
@AndresF。仅当程序所编写的语言子集能够表达不暂停的程序时,程序是否暂停才是未知的。如果您的程序是用C语言编写的,而没有使用无限制的循环/递归/ setjmp等,或者是用Coq或ESSL编写的,则它必须停止并且可以跟踪所有路径。(
转弯

Answers:


128

如果测试了程序的每条路径,是否可以保证找到所有错误?

没有

如果没有,为什么不呢?如何解决程序流程的每一种可能的组合,如果存在的话却找不到问题?

因为即使测试了所有可能的路径,仍然没有用所有可能的或所有可能的值组合测试它们。例如(伪代码):

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年,它已经相当现在已经超过二十年。)


7
@digitgopher:我想,但是如果一个程序没有输入,它有什么用呢?
梅森·惠勒

34
还可能会丢失集成测试,测试中的错误,依赖项中的错误,构建/部署系统中的错误或原始规范/需求中的错误。您永远不能保证找到所有错误。
Ixrec 2015年

11
@Ixrec:不过,SQLite付出了极大的努力! 但是看看这是多么巨大的努力!不能很好地扩展到大型代码库。
梅森惠勒2015年

13
您不仅不会测试所有可能的值或它们的组合,还没有测试所有的相对计时,其中一些可能会暴露出竞争状况,或者确实使您的测试陷入僵局,从而使它无法报告任何内容。甚至不会失败!
Iwillnotexist Idonotexist 2015年

14
我的回忆(得到诸如此类文字的支持)是Dijkstra认为,在良好的编程实践中,程序(在所有条件下)正确的证明首先应该是程序开发的组成部分。从这个观点来看,测试像炼金术。我认为这不是夸大其词,而是用非常强烈的语言表达了非常强烈的意见。
David K

71

除了梅森的答案之外,还有另一个问题:覆盖率不能告诉您测试了什么代码,而是告诉您执行了什么代码。

假设您有一个测试套件,其路径覆盖率为100%。现在删除所有断言,然后再次运行测试套件。测试者Voilà仍然具有100%的路径覆盖率,但它绝对不会测试任何东西。


2
它可以确保在调用测试的代码(带有测试中的参数)时没有异常。这比什么都没有。
圣保罗Ebermann

7
@PaŭloEbermann同意,一点都没有。但是,它远不及“发现所有错误”;)
Andres F.

1
@PaŭloEbermann:异常是代码路径。如果代码可以抛出但某些测试数据没有抛出,则该测试不会达到100%的路径覆盖率。这并非异常作为错误处理机制而特定。Visual Basic ON ERROR GOTO也是C的路径if(errno)
MSalters 2015年

1
@MSalters我在说的是代码(按规范),无论输入如何,都不应引发任何异常。如果抛出任何异常,那将是一个错误。当然,如果您指定了引发异常的代码,则应该对其进行测试。(当然,正如Jörg所说的,仅检查代码没有引发异常通常不足以确保它做正确的事情,即使对于非抛出代码也是如此)。 -可见的代码路径,例如用于空指针取消引用或被零除。您的路径覆盖工具会抓住那些吗?
圣保罗Ebermann

2
这个答案很重要。我会进一步说,因此,路径覆盖范围无法保证甚至找到一个错误。有迹象表明,可以保证至少该变化将被检测的指标,但是-突变检测实际上可以保证代码的(一些)的修改被检测到。
eis 2015年

34

这是一个更简单的示例,可以四舍五入。考虑以下排序算法(在Java中):

int[] sort(int[] x) { return new int[] { x[0] }; }

现在,让我们测试一下:

sort(new int[] { 0xCAFEBABE });

现在,请考虑(A)此特定调用sort返回正确的结果,(B)此测试已涵盖所有代码路径。

但是,显然,该程序实际上并未排序。

因此,所有代码路径的覆盖范围不足以保证程序没有错误。


12

考虑该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

2
这是另一个通过测试的实现(请原谅非断行的Python):def abs(x): if x == -3: return 3 else: return 0您可能会抽出else: return 0零件并获得100%的覆盖率,但是即使该功能确实通过了单元测试,该函数实际上也没有用。
CVn 2015年

7

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

如果您的测试人员可以为此生成所有路径,那么恭喜您是密码学家。


容易获得足够小的整数:)
CodesInChaos 2015年

在不了解任何信息的情况下cryptohash,很难说“足够小”是什么。在超级计算器上可能需要两天才能完成。但是,是的,int可能会有点小short
dureuill 2015年

使用32位整数和典型的加密哈希(SHA2,SHA3等),这应该很便宜。几秒钟左右。
CodesInChaos

7

从其他答案中可以很明显地看出,测试中100%的代码覆盖率并不意味着100%的代码正确性,甚至并不意味着所有可以通过测试发现的错误都将被发现(不要介意没有任何测试可以捕获的错误)。

回答此问题的另一种方法是实践中的一种方法:

在现实世界中,实际上在您自己的计算机上,有许多软件是使用一组可以提供100%覆盖率的测试开发的,但是仍然存在错误,包括可以更好地测试的错误。

因此,一个必然的问题是:

代码覆盖率工具的重点是什么?

代码覆盖率工具可帮助您确定一个被忽略测试的区域。可能很好(即使未经测试,代码也正确无误),可能无法解决(由于某种原因,无法找到路径),或者可能是现在或以后进行修改的臭臭虫的所在地。

在某些方面,拼写检查是可比较的:某些东西可以“通过”拼写检查,并且会以与词典中的单词匹配的方式被拼错。否则它可能会“失败”,因为词典中没有正确的单词。否则,它可以通过并完全是胡说八道。拼写检查是一种工具,可以帮助您确定在校对过程中可能错过的地方,但正如它不能保证进行完整正确的校对一样,因此代码覆盖范围也不能保证进行完整正确的测试。

当然,错误地使用拼写检查的方法是众所周知的,它伴随着每条建议母羊的建议,因此,如果母羊把钱借给它,那么回避就变得更糟了。

使用代码覆盖率可能会很诱人,尤其是如果您有近乎完美的98%的情况下,填写案例以使剩余的路径被点击。

这等同于使用拼写检查进行纠正,即所有单词都是风雨无阻,或者都是所有合适的单词。结果就是一团糟。

但是,如果考虑未覆盖路径真正需要的测试,则代码覆盖工具将完成其工作。不是保证您的正确性,而是指出了一些需要完成的工作。


+1我喜欢这个答案,因为它具有建设性,并提到了覆盖的一些好处。
Andres F.

4

路径覆盖无法告诉您是否已实现所有必需的功能。遗漏某个功能是一个错误,但是路径覆盖不会检测到它。


1
我认为这取决于错误的定义。我认为不应将缺少的功能视为错误。
eis 2015年

@eis-您看不到产品说明其X确实没有的问题,而该产品没有X?那是“ bug”的狭窄定义。当我为Borland的C ++产品线管理质量检查时,我们并不慷慨。
皮特·贝克尔

我不明白为什么会说文档它X,如果这是从来没有实现过
EIS

@eis-如果原始设计需要功能X,则文档最终可能会描述功能X。如果没有人实现,那就是一个错误,并且路径覆盖(或任何其他黑盒测试)都找不到它。
皮特·贝克尔

哎呀,路径覆盖是白箱测试,而不是黑匣子。白盒测试无法捕获缺少的功能。
皮特·贝克尔

4

问题的部分原因是100%的覆盖率仅保证代码在一次执行后即可正常运行。诸如内存泄漏之类的某些错误可能在一次执行后就不明显或不会引起问题,但是随着时间的流逝会给应用程序造成问题。

例如,假设您有一个连接到数据库的应用程序。也许用一种方法,程序员在完成查询后会忘记关闭与数据库的连接。您可以对该方法进行几次测试,但不会发现其功能有任何错误,但是您的数据库服务器可能会遇到无法连接的情况,因为该特定方法完成后并没有关闭连接,打开的连接必须现在超时。


同意这是问题的一部分,但真正的问题比这更根本。即使使用具有无限内存且没有并发性的理论计算机,100%的测试覆盖率也并不意味着没有错误。此处的答案中有很多琐碎的示例,但这是另一个示例:如果我的程序是times_two(x) = x + 2,则测试套件将完全覆盖它assert(times_two(2) == 4),但这显然仍然是错误的代码!不需要内存泄漏:)
Andres F.

2
这是一个很好的观点,我认识到,这是解决无错误应用程序可能性的一个更大/更基本的主旨,但是正如您所说的那样,它已经在此处添加,我想添加一个未在其中涵盖的内容现有答案。我听说应用程序崩溃是因为不再需要不再需要将数据库连接释放回连接池时–内存泄漏只是资源管理不善的典型示例。我的观点是补充说,一般来说,对资源的正确管理不能完全通过测试。
Derek W

好点子。同意
Andres F.

3

如果测试了程序的每条路径,是否可以保证找到所有错误?

如前所述,答案是否定的。

如果没有,为什么不呢?

除了所说的以外,还存在不同级别的错误,这些错误无法通过单元测试进行测试。仅举几例:

  • 集成测试中捕获的错误(单元测试毕竟不应该使用实际资源)
  • 需求中的错误
  • 设计和建筑中的错误

2

对每条路径进行测试意味着什么?

其他答案很好,但我只想补充一下,“测试程序的每条路径”的条件本身都是模糊的。

考虑以下方法:

def add(num1, num2)
  foo = "bar"  # useless statement
  $global += 1 # side effect
  num1 + num2  # actual work
end

如果您编写一个断言的测试,则add(1, 2) == 3代码覆盖率工具将告诉您每一行都在执行。但是您实际上尚未断言有关全局副作用或无用分配的任何信息。这些行已执行,但尚未经过实际测试。

变异测试将有助于发现此类问题。突变测试工具将提供一系列“突变”代码并查看测试是否仍通过的预定方法。例如:

  • 一种突变可能会将更改+=-=。该突变不会导致测试失败,因此可以证明您的测试对全局副作用没有任何意义。
  • 另一个突变可能会删除第一行。该突变不会导致测试失败,因此可以证明您的测试没有断言关于分配的任何有意义的信息。
  • 另一个突变可能会删除第三行。这将导致测试失败,在这种情况下,这表明您的测试确实断言了该行的某些内容。

本质上,变异测试是测试您的测试的一种方法。但是就像您永远不会使用所有可能的输入集来测试实际功能一样,您也永远不会运行每种可能的变异,因此同样,这是有限的。

我们可以做的每个测试都是向无错误程序迈进的启发。没有什么是完美的。


0

好吧... 是的,实际上,如果“通过”程序的每条路径都经过测试。但这意味着,贯穿程序可能具有的所有可能状态的整个空间的所有可能路径,包括所有变量。即使对于非常简单的静态编译程序(例如,旧的Fortran数字处理程序),这也不可行,尽管至少可以想象得到:如果只有两个整数变量,则基本上是在处理连接点的所有可能方式。二维网格;实际上看起来很像旅行推销员。对于n个这样的变量,您要处理的是n维空间,因此对于任何实际程序而言,任务是完全难以处理的。

更糟的是:严重的东西,你只是原始变量固定数量,但在函数调用动态创建变量,或在图灵完备的语言具有可变大小的变量...之类的东西,尽可能。这就给状态空间带来了无限的空间,即使拥有了非常强大的测试设备,也打破了所有覆盖范围的希望。


就是说...实际上情况并不那么暗淡。这可能的坡口整个计划是正确的,但你必须放弃一些想法。

首先:强烈建议改用声明性的languange。出于某种原因,命令式语言一直以来都是最受欢迎的语言,但是它们将算法与实际交互结合在一起的方式使说“正确”的含义变得极其困难。

纯函数式编程语言中,这要容易得多:它们在数学函数的真正有趣特性和您无法真正说出的模糊现实世界交互之间有着明显的区别。对于函数,很容易指定“正确的行为”:如果对于所有可能的输入(从参数类型)得出相应的期望结果,则函数的行为正确。

现在,您说这仍然很棘手...毕竟,所有可能参数的空间通常也是无限维的。没错–尽管对于单个功能,即使是简单的覆盖率测试也可以使您比命令式程序所希望的还要遥遥领先!但是,有一个不可思议的强大工具可以改变游戏规则:通用量化/ 参数多态性。基本上,这使您可以对非常通用的数据类型编写函数,并确保如果它仅适用于简单的数据示例,则它将完全适用于任何可能的输入。

至少在理论上。要找到真正通用的正确类型并不容易,您可以完全证明这一点–通常,您需要一种依赖类型的语言,而且这些语言往往很难使用。但是仅凭参数多态性以功能风格进行编写已经极大地提高了您的“安全级别”-您不一定会发现所有错误,但必须将它们很好地隐藏起来,以便编译器不会发现它们!


我不同意你的第一句话。遍历程序的每个状态本身并不会检测到任何错误。即使您检查崩溃和显式错误,也仍然不会以任何方式检查实际功能,因此您仅覆盖了错误空间的一小部分。
Matthew阅读

@MatthewRead:因此,如果应用此方法,则“错误空间”是所有状态空间的适当子空间。当然这是假设的,因为即使“正确”的状态也构成了太大的空间,无法进行详尽的测试。
大约
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.