如何使用图形结构对代码进行单元测试?


18

我正在编写(递归)代码来浏览依赖关系图,以查找依赖关系中的循环或矛盾。但是,我不确定如何进行单元测试。问题是我们主要关心的问题之一是将对可能出现的所有有趣的图结构进行代码处理,并确保所有节点都将得到适当处理。

尽管通常100%的行或分支覆盖率足以确保某些代码可以工作,但即使100%的路径覆盖率,您仍然会有疑问。

因此,如何为测试用例选择图形结构,以确保其代码可以处理您在真实数据中发现的所有可能的排列。


PS-如果重要的话,我图中的所有边都标记为“必须具有”或“不能具有”,并且没有琐碎的循环,并且任何两个节点之间只有一条边。


PPS-此附加问题声明最初由问题的作者在以下评论中发布:

For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.


13
对任何其他方法进行单元测试的方式相同。您为每种方法标识所有“有趣的”测试用例,并为它们编写单元测试。对于您的情况,您必须为每个“有趣的”图结构创建固定的依赖图。
Dunk

@Dunk我们一直认为我们涵盖了所有棘手的问题,然后我们意识到某种结构会导致我们之前未曾考虑过的问题。测试每一个棘手的是我们能想到的就是我们正在做的,就是我希望能找到一些指引/程序产生麻烦也许使用的基本形式还原等例子

6
这是任何形式的测试的问题。您所知道的只是您认为可行的测试。这并不意味着您的sw仅因为测试通过而没有错误。每个项目都有同样的问题。我正处于交付当前项目的最后阶段,因此我们可以开始制造。我们现在遇到的错误类型往往比较模糊。例如,当硬件仍然可以达到规格但勉强达到标准时,当与其他硬件同时出现相同问题时,就会出现问题。但仅在某些情况下:( sw经过了充分的测试,但我们并未考虑所有内容
Dunk 2014年

您所描述的听起来更像是集成测试,而不是单元测试。单元测试将确保一种方法能够找到图中的圆。其他单元测试将确保被测类可以处理特定图形的特定圆。
SpaceTrucker

周期检测是一个广为人知的主题(请参阅Knuth,以及下面的一些解答),解决方案不涉及大量特殊情况,因此您首先应确定是什么使您的问题引起这种情况。是因为您提到的矛盾?如果是这样,我们需要有关它们的更多信息。如果是实现选择的结果,则可能必须进行较大的重构。从根本上讲,这是一个设计问题,您必须仔细考虑一下,TDD是错误的方法,可能会使您在走到尽头之前进入迷宫。
sdenham

Answers:


5

我们一直认为我们涵盖了所有棘手的问题,然后我们意识到某种结构会导致我们以前没有考虑过的问题。测试我们能想到的每一个棘手的事情就是我们在做什么。

听起来是个不错的开始。我想您已经尝试过应用一些经典技术,例如边界值分析等效划分,就像您已经提到的基于覆盖率的测试一样。在花费大量时间来构建好的测试用例之后,您到了一个地步,您,您的团队以及您的测试人员(如果有的话)都耗尽了点子。这就是您应该离开单元测试的路径,并开始使用尽可能多的实际数据进行测试的关键所在。

很明显,您应该尝试从生产数据中选择大量的图形。也许您仅需要为该过程的那部分编写一些其他工具或程序。这里最困难的部分可能是验证程序输出的正确性,当您在程序中放置一万个不同的真实世界图形时,如何知道程序始终产生正确的输出?显然,您无法手动检查。因此,如果幸运的话,您可以对依赖检查进行第二次非常简单的实现,这可能无法满足您的性能期望,但是比原始算法更容易验证。您还应该尝试将很多真实性检查直接集成到程序中(例如,

最后,学会接受每个测试只能证明存在错误,而不能证明没有错误。


5

1.随机测试生成

编写一个生成图的算法,让它生成几百个(或更多)随机图,并将每个图扔给您的算法。

保留会引起有趣故障的图形的随机种子,并将其添加为单元测试。

2.硬编码棘手的部分

您知道有些棘手的图形结构可以立即进行编码,或编写一些将它们组合在一起并将其推入算法的代码。

3.生成详尽清单

但是,如果要确保“代码可以处理您在现实世界数据中发现的所有可能的排列。”,则需要从所有种子中遍历所有的排列,而不是从随机种子生成此数据。(这是在测试地铁轨道信号系统时完成的,并且会给您带来大量需要时间测试的情况。对于地铁地铁,该系统是有界的,因此排列数量有上限。不确定您的情况如何适用)


提问者写道,他们无法告知他们是否已考虑所有情况,这意味着没有办法枚举它们。直到他们足够了解问题域才能做到这一点,如何测试才是一个有争议的问题。
sdenham

@sdenham您如何枚举从字面上看可能有无限数量的有效组合?我希望找到一些类似的东西:“这些是最棘手的图形结构,通常会在实现中捕获错误”。我很了解域,因为它很简单:For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.域不是问题。
雪橇

@ArtB:感谢您对问题的澄清。正如您已经说过的,在任何两个顶点之间不超过一个边,并且显然排除了带有循环的路径(或者在任何一个循环中至少有一个以上的通过),那么至少我们知道字面上没有无限个可能的有效组合,这是进步。注意,知道如何列举所有可能性与说必须做的事情不同,因为这可能是提出正确性论点的起点,而后者又可以指导测试。我会多加考虑...
sdenham 2015年

@ArtB:您应该修改问题以包括对此处给出的问题说明的更新。同样,它可能有助于说明这些是有向边(如果是这种情况),以及是否将循环视为图形中的错误,而不仅仅是算法需要处理的情况。
sdenham

4

在这种情况下,没有任何测试是足够的,甚至没有大量的现实世界数据或模糊测试。100%的代码覆盖率,甚至100%的路径覆盖率不足以测试递归函数。

递归函数可以证明是正式的证明(在这种情况下应该不会那么困难),或者不是。如果该代码与特定于应用程序的代码过于纠结而无法排除副作用,那么这就是开始的地方。

该算法本身听起来像是一种简单的泛洪算法,类似于简单的广泛的优先搜索,并且添加了一个黑名单,该黑名单不得与从所有节点运行的访问节点列表相交。

foreach nodes as node
    foreach nodes as tmp
        tmp.status = unmarked

    tovisit = []
    tovisit.push(node)
    node.status = required

    while |tovisit| > 0 do
        next = tovisit.pop()
        foreach next.requires as requirement
            if requirement.status = unmarked
                tovisit.push(requirement)
                requirement.status = required
            else if requirement.status = blacklisted
                return false
        foreach next.collides as collision
            if collision.status = unmarked
                requirement.status = blacklisted
            else if requirement.status = required
                return false
return true

对于任何结构的图,此迭代算法均满足以下条件:对于任何结构的图,不需要任何依赖关系,并且将其列入黑名单。

尽管它可能不如您自己的实现快,但可以证明它在所有情况下都会终止(至于外循环的每次迭代,每个元素只能推到 tovisit队列),它淹没了整个可达对象图(归纳证明),它会检测从每个节点开始同时需要人工制品和将其列入黑名单的所有情况。

如果可以证明自己的实现具有相同的特征,则可以证明其正确性而无需进行单元测试。仅需要测试用于显示从队列中弹出和弹出,计数队列长度,遍历属性等的基本方法,并且显示出没有副作用。

编辑:该算法不能证明的是您的图形没有周期。有向无环图虽然是一个经过充分研究的主题,所以找到一种现成的算法来证明这种性质也应该很容易。

如您所见,根本不需要重新发明轮子。


3

您正在使用“所有有趣的图结构”和“正确处理”之类的短语。除非您有办法针对所有这些结构测试代码并确定代码是否正确处理了图形,否则只能使用诸如测试覆盖率分析之类的工具。

我建议您首先查找并测试许多有趣的图形结构,然后确定适当的处理方式,然后看代码能做到这一点。然后,您可以开始将这些图扰乱成a)违反规则的残破图或b)不太有趣的有问题的图;查看您的代码是否正确无法处理它们。


尽管这是一种很好的测试方法,但它并不能解决问题的核心问题:如何确保涵盖所有案例。我认为,这将需要更多分析并可能进行重构-请参阅上面的问题。
sdenham 2015年


2

当涉及到这种难以测试的算法时,我会选择TDD,您可以在其中基于测试构建算法,

简而言之,

  • 编写测试
  • 看到它失败了
  • 修改代码
  • 确保所有测试通过
  • 重构

然后重复循环

在这种情况下

  1. 第一个测试是单节点图,其中算法不应返回任何周期
  2. 第二个是没有循环的三节点图,其中算法不应返回任何循环
  3. 下一步骤是将三个节点图与一个循环一起使用,其中算法不应返回任何循环
  4. 现在,您可以根据可能的情况在更复杂的周期中进行测试

此方法的一个重要方面是,您需要始终为可能的步骤添加测试(将可能的场景分为简单的测试),并且当您涵盖所有可能的场景时,算法通常会自动演化。

最后,您需要添加一个或多个复杂的集成测试,以查看是否存在任何无法预料的问题(例如,当图形很大且使用递归时,堆栈溢出错误/性能错误)


2

我对问题的理解,如最初所述,然后在Macke的答复下通过评论进行了更新,包括以下内容:1)两种边缘类型(依赖性和冲突)都是有针对性的;2)如果两个节点通过一条边连接,则即使它们是另一种类型或相反类型,也不得通过另一条边连接;3)如果可以通过混合不同类型的边来构造两个节点之间的路径,那么这是错误的,而不是被忽略的情况;4)如果两个节点之间使用一种类型的边缘存在一条路径,则它们之间可能不会使用另一种类型的边缘存在另一条路径;5)不允许使用单边沿类型或混合边沿类型的循环(从应用程序的猜测来看,我不确定仅冲突循环是一个错误,但是可以消除此条件,如果不允许的话)。

此外,我将假设所使用的数据结构不会阻止违反这些要求的表述(例如,如果始终从节点对到(类型,方向)的映射中无法表达违反条件2的图)首先具有最少编号的节点。)如果无法表达某些错误,则会减少要考虑的案例数。

实际上,这里可以考虑三个图:两个是唯一一个边缘类型的图,以及由这两个类型中的每一个的并集形成的混合图。您可以使用它来系统地生成多达一定数量节点的所有图。首先生成N个节点的所有可能图,其中任意两个有序节点对之间的边缘不超过一个边(有序对,因为它们是有向图)。现在取这些图的所有可能对,一个代表依赖关系,另一个代表冲突,形成每对的结合。

如果您的数据结构不能表达违反条件2的条件,则可以通过仅构造适合于依赖关系图空间的所有可能的冲突图来大大减少要考虑的情况,反之亦然。否则,您可以在形成联合时检测到违反条件2的情况。

在从第一个节点开始的广度优先遍历组合图时,您可以构建到每个可到达节点的所有路径的集合,并且这样做时,可以检查是否违反了所有条件(对于循环检测,您可以使用Tarjan的算法。)

即使图形断开连接,您也只需考虑来自第一个节点的路径,因为在某些其他情况下,来自任何其他节点的路径将显示为来自第一个节点的路径。

如果可以简单地忽略混合边缘路径,而不是将其视为错误(条件3),则只需独立考虑依赖关系图和冲突图,并检查一个节点是否可达,而另一个节点则不可达。

如果您记得在检查N-1个节点的图形中找到的路径,则可以将其用作生成和评估N个节点的图形的起点。

这不会在节点之间生成相同类型的多个边,但是可以对其进行扩展。但是,这将大大增加案例的数量,因此,如果要测试的代码使这种情况无法表示或失败,则可以事先过滤掉所有此类案例会更好。

编写这样的oracle的关键是,即使它意味着效率低下,也要使其尽可能简单,以便您可以建立对它的信任(理想情况下是通过对其正确性的论证,并通过测试进行备份)。

一旦有了生成测试用例的方法,并且相信创建的Oracle可以准确地将好与坏分开,就可以使用它来驱动目标代码的自动化测试。如果那不可行,那么您的下一个最佳选择是针对特殊情况梳理结果。oracle可以对发现的错误进行分类,并为您提供有关可接受情况的一些信息,例如每种类型的路径的数量和长度,以及两种路径的开头是否都存在节点,并且可以帮助您寻找以前从未见过的案例。

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.