如何对复杂方法进行单元测试

26
我有一个方法,它接受一个北向角度和一个方位角度,并从8个可能的值(如North,NorthEast,East等)返回指南针点值。我想创建一个单元测试,为此提供不同的北向角度和方位角度值,以确保具有足够的覆盖面积,使我有信心我的方法有效。
我的初始尝试生成了从-360到360的所有可能的整数值,同时测试了每个从-360到360的方位角度值。然而,我的测试代码最终成为了我正在测试的代码的另一种实现。这让我想知道对于这种情况,最好的测试是什么样的,使得我的测试代码不会包含与我的生产代码可能相同的错误。
我的当前解决方案是花时间编写一个带有数据点和预期结果的XML文件,在测试期间可以读取该文件并用于验证该方法,但这似乎非常耗时。我不想编写一个包含与我的原始测试相同值范围的文件(这将是大量的XML),但我确实想包含足够的内容来充分测试该方法。
  • 如何测试一个方法而不只是重新实现该方法?
  • 如何在不必为所有可能的输入和结果设置测试点的情况下实现足够的覆盖面以对我正在测试的方法具有信心?
显然,不要过多地关注我的具体例子,因为这适用于许多涉及复杂计算和数据范围的情况。
注意:我正在使用Visual Studio和C#,但我认为这个问题是语言无关的。

你是如何生成测试值的?在我看来,假设原始计算中存在错误,你需要一个可靠的公式来生成这些测试值。 - Salsero69
10个回答

30

首先,你是正确的,你不希望测试代码重复执行与被测试代码相同的计算。其次,你的第二个方法是朝着正确方向迈出的一步。你的测试应该包含一组特定的输入和为这些输入预先计算的期望输出值。

你的 XML 文件应该只包含你描述的一部分输入数据。你的测试应该确保你可以处理输入域的极端范围(-360,360),一些仅在范围末端内的数据点以及一些中间的数据点。你的测试还应检查当给定超出输入范围的值时,你的代码是否会正常失败(例如-361和+361)。

最后,在你的特定情况下,你可能需要更多的边界情况,以确保你的函数正确处理有效输入范围内的“切换”点。这些将是你输入数据中预期输出从“北”到“西北”和从“西北”到“西”等的点。(不要运行你的代码来寻找这些点,请手动计算)。

只集中测试这些边缘情况和一些边缘之间的情况,应该大大减少你需要测试的点的数量。


5
我喜欢进行以下操作。
  1. Create a spreadsheet with right answers. However complex it needs to be is irrelevant. You just need some columns with the case and some columns with the expected results.

    For your example, this can be big. But big is okay. You'll have an angle, a bearing and the resulting compass point value. You may have a bunch of intermediate results.

  2. Create a small program that reads the spreadsheet and writes the simplified, bottom-line unittest cases. You want your cases stripped down to

    def testCase215n( self ):
        self.fixture.setCourse( 215 )
        self.fixture.setBearing( 45 )
        self.fixture.calculate()
        self.assertEquals( "N", self.fixture.compass() )
    

[这就是Python,同样的方法也适用于C#。]

电子表格包含唯一权威的正确答案列表。你只需要从中生成一到两次代码。当然,如果你发现电子表格版本中存在错误,你需要修复它。

我使用一个带有 xlrd 和 Mako 模板生成器的小 Python 程序来实现这一点。您也可以使用类似的 C# 产品。


似乎可以通过将数据放入列表中,在运行时迭代它以调用测试,而不是生成代码来更简单地完成此操作。 - Ned Batchelder
@Ned Batchelder:绝对正确。也许更简单。但我更喜欢我的单元测试只做单元测试。一个读取文件或计算首选答案的单元测试对我来说不太“合适”。这不是一个很好的理由,是吗? - S.Lott

5
你可以将这个方法重构成更容易进行单元测试的部分,并为这些部分编写单元测试。然后整个方法的单元测试只需要关注集成问题即可。

很难想象将其重构为更简单的方法。我知道原帖中将其描述为“复杂”,但对我来说听起来相当简单。 - Ned Batchelder
1
是的,基本问题非常简单。我在提到可能的输入和结果时使用了“复杂”的术语。然而,在其他问题中,重构肯定是简化单元测试的一种选择。 - Jeff Yates

4
如果你能够想到一种完全不同的方法实现你的函数,并且这个方法会有完全不同的错误隐藏点,那么你可以通过对比测试来验证结果的正确性。通常我在拥有高效但复杂实现的情况下,会尝试使用低效但简单易懂的实现方式进行对比测试。例如,在编写哈希表实现时,我可能会实现一个基于线性搜索的关联数组以便进行测试,并使用大量随机生成的输入数据进行测试。线性搜索关联数组很难出错,即使出错了也不容易和哈希表出现相同的错误。因此,如果哈希表的观察行为与线性搜索关联数组相同,我相信它就是正确的。
其他例子包括编写冒泡排序来测试堆排序,或者使用已知可工作的排序函数来查找中位数,并将其与O(N)中位数查找算法实现的结果进行比较。

我曾考虑过这个选项,但它让我感觉不对。我已经在其他输入变化较少的单元测试中使用过这个选项。 - Jeff Yates

3

我认为你的解决方案很好,尽管使用了XML文件(我会使用纯文本文件)。但更常用的策略是测试极限情况,比如在你的情况下,使用-360、360、-361、361和0作为输入值。


1
你可以尝试使用正交数组测试来实现对所有可能的组合进行全覆盖,而不是单独对每一种情况进行测试。这是一种基于统计学原理的技术,因为大多数错误通常是由两个参数之间的交互导致的。使用这种方法可以极大地减少您编写的测试用例数量。

1

不确定您的代码有多复杂,如果它接收一个整数并将其分成8或16个方向,那么可能只需要几行代码,对吗?

如果您测试代码的方式不同,那么您很难不重写代码进行测试。理想情况下,您希望独立的第三方根据相同的要求编写测试代码,但不查看或借用您的代码。在大多数情况下,这是不太可能发生的。在这种情况下,这可能过于严格。

在这种特定情况下,我会按顺序将每个数字从-360到+360输入,并打印数字和结果(以可以编译为头文件的格式保存到文本文件中)。目视检查方向是否在所需输入处更改。这应该很容易进行目视检查和验证。现在您拥有了一张输入和有效输出的表格。接下来,让程序随机从有效输入中选择并将其馈入您的测试代码,然后查看是否得出正确答案。进行几百次这样的随机测试。在某些时候,您需要验证小于-360或大于+360的数字是否按照您的要求处理,我假设是剪切或调制。


0

伪代码:

array colors = { red, orange, yellow, green, blue, brown, black, white }
for north = -360 to 361
    for bearing = -361 to 361
        theColor = colors[dirFunction(north, bearing)] // dirFunction is the one being tested
        setColor (theColor)
        drawLine (centerX, centerY,
                  centerX + (cos(north + bearing) * radius),
                  centerY + (sin(north + bearing) * radius))
        Verify Resulting Circle against rotated reference diagram.

当North = 0时,您将获得一个8色饼图。随着North的变化(+或-),饼图看起来相同,但是旋转了那么多度。测试验证很简单,只需确保(a)图像是正确旋转的饼图,(b)橙色区域中没有绿色斑点等。
顺便说一下,这种技术是世界上最好的调试工具之一:让计算机为您绘制=IT=认为它正在执行的操作的图片。(开发人员经常浪费数小时追踪他们认为计算机正在执行的操作,只发现它正在执行完全不同的操作。)

这是手动测试的好主意,但对于自动化单元测试来说并不是很有用。我喜欢你的想法,我同意,让计算机绘制它正在做的事情的图片是一个非常强大的工具。 - Jeff Yates
这当然可以自动化。此外,它完全解决了“在测试中不要使用与代码相同的算法”的问题。对于半自动化,您可以通过视觉检查一组参考图像,然后将其保存到“正确答案”缓存中。 - Olie
你如何自动化这个测试,以便不需要人为地评估结果?进行图像比较?你如何确保图像比较的正确性? - Jeff Yates

0

我参加了一门软件测试课程链接文本,基本上你想要做的就是确定输入的类别..所有实数?所有整数,只有正数,只有负数等等...然后分组输出操作。360与359是否完全不同,或者它们最终对应于应用程序执行相同的操作。一旦完成,进行输入到输出的组合。

这一切似乎都很抽象和模糊,但在提供方法代码之前,很难想出完美的策略。

另一种方法是进行分支级别测试或谓词覆盖测试。代码覆盖率并不是万无一失的,但不覆盖所有代码似乎是不负责任的。


在开发单元测试时,不需要查看方法的代码。如果你将测试编写为实现,则无法正确测试需求。我认为我提出的问题与我给出的示例无关,因此我省略了细节。谢谢。 - Jeff Yates

0

一种方法,可能需要与其他测试方法结合使用,是尝试制作一个反转正在测试的方法的函数。在这种情况下,它将采用罗盘方向(比如东北),并输出一个方位角(给定北方的方位角)。然后,您可以通过将其应用于一系列输入,然后应用反转方法的函数来测试该方法,并查看是否返回原始输入。

存在复杂性,特别是如果一个输出对应多个输入,但在这些情况下,可能会生成与给定输出相对应的输入集,并测试集合的每个成员(或集合元素的某个样本)。

这种方法的优点是,它不依赖于您能否手动模拟该方法,或创建该方法的替代实现。如果反转涉及到与原始方法使用的问题不同的方法,它应该减少在两者中都出现等效错误的风险。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接