13.6. 健全性测试

通常,您会发现一个代码单元包含一组互逆函数,通常以转换函数的形式出现,其中一个函数将 A 转换为 B,另一个函数将 B 转换为 A。在这些情况下,创建一个“健全性检查”非常有用,以确保您可以将 A 转换为 B 并返回 A,而不会损失精度、产生舍入误差或触发任何其他类型的错误。

考虑此需求

  1. 如果您取一个数字,将其转换为罗马数字,然后再将其转换回数字,则应该得到与开始时相同的数字。因此,对于 1..3999 中的所有 nfromRoman(toRoman(n)) == n

示例 13.5. 测试 toRomanfromRoman


class SanityCheck(unittest.TestCase):        
    def testSanity(self):                    
        """fromRoman(toRoman(n))==n for all n"""
        for integer in range(1, 4000):        1 2
            numeral = roman.toRoman(integer) 
            result = roman.fromRoman(numeral)
            self.assertEqual(integer, result) 3
1 您之前已经见过range 函数,但这里它使用两个参数调用,返回一个整数列表,从第一个参数 (1) 开始,连续计数到第二个参数 (4000),但不包括第二个参数。因此,1..3999 是转换为罗马数字的有效范围。
2 我只想顺便提一下,integerPython 中不是关键字;这里它只是一个变量名,和其他变量名一样。
3 这里的实际测试逻辑很简单:取一个数字 (integer),将其转换为罗马数字 (numeral),然后将其转换回数字 (result),并确保最终得到的数字与开始时相同。如果不同,assertEqual 将引发异常,测试将立即被视为失败。如果所有数字都匹配,assertEqual 将始终静默返回,整个 testSanity 方法最终将静默返回,测试将被视为通过。

最后两个需求与其他需求不同,因为它们看起来既随意又琐碎

  1. toRoman 应始终使用大写字母返回罗马数字。
  2. fromRoman 应仅接受大写罗马数字( 当给定小写输入时,它应该失败)。

事实上,它们在某种程度上是随意的。例如,您可以规定 fromRoman 接受小写和混合大小写输入。但它们并非完全随意;如果 toRoman 始终返回大写输出,则 fromRoman 必须至少接受大写输入,否则“健全性检查”(需求 #6)将失败。它接受大写输入的事实是随意的,但正如任何系统集成商都会告诉您的那样,大小写始终很重要,因此最好预先指定行为。如果值得指定,就值得测试。

示例 13.6. 测试大小写


class CaseCheck(unittest.TestCase):                   
    def testToRomanCase(self):                        
        """toRoman should always return uppercase"""  
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            self.assertEqual(numeral, numeral.upper())         1

    def testFromRomanCase(self):                      
        """fromRoman should only accept uppercase input"""
        for integer in range(1, 4000):                
            numeral = roman.toRoman(integer)          
            roman.fromRoman(numeral.upper())                   2 3
            self.assertRaises(roman.InvalidRomanNumeralError,
                              roman.fromRoman, numeral.lower())   4
1 这个测试用例最有趣的地方在于它没有测试的所有东西。它没有测试从 toRoman 返回的值是否正确,甚至没有测试它是否一致;这些问题由单独的测试用例回答。您有一个完整的测试用例只是为了测试大写字母。您可能会想将其与健全性检查结合起来,因为两者都遍历了整个值范围并调用了 toRoman[6] 但这将违反基本规则之一:每个测试用例应该只回答一个问题。想象一下,您将此大小写检查与健全性检查结合起来,然后该测试用例失败了。您需要进行进一步的分析,以找出测试用例的哪一部分失败了,才能确定问题所在。如果您需要分析单元测试的结果才能弄清楚它们的含义,那么这肯定表明您对测试用例的设计有误。
2 这里有一个类似的教训需要学习:即使“您知道toRoman 始终返回大写字母,但您在这里还是明确地将其返回值转换为大写字母,以测试 fromRoman 是否接受大写输入。为什么?因为 toRoman 始终返回大写字母这一事实是一个独立的需求。如果您更改了该需求,例如,它始终返回小写字母,则 testToRomanCase 测试用例需要更改,但此测试用例仍然有效。这是基本规则的另一个:每个测试用例必须能够独立于任何其他测试用例工作。每个测试用例都是一个孤岛。
3 请注意,您没有将 fromRoman 的返回值赋给任何变量。这是 Python 中的合法语法;如果一个函数返回一个值,但没有人监听,Python 就会丢弃该返回值。在本例中,这正是您想要的。此测试用例不测试返回值的任何内容;它只是测试 fromRoman 是否接受大写输入而不引发异常。
4 这是一行复杂的代码,但它与您在 ToRomanBadInputFromRomanBadInput 测试中所做的非常相似。您正在测试以确保使用特定值(numeral.lower(),循环中当前罗马数字的小写版本)调用特定函数(roman.fromRoman)会引发特定异常(roman.InvalidRomanNumeralError)。如果它确实如此(每次循环都如此),则测试通过;如果即使有一次它做了其他事情(例如引发了不同的异常,或者在没有引发任何异常的情况下返回了一个值),则测试失败。

在下一章中,您将看到如何编写通过这些测试的代码。

脚注

[6] 我能抗拒一切,除了诱惑。”——奥斯卡·王尔德