13.6. 完备性检测 (Testing for sanity)

你经常会发现一组代码中包含互逆的转换函数,一个把 A 转换为 B ,另一个把 B 转换为 A。在这种情况下,创建“完备性检测”可以使你在由 A 转 B 再转 A 的过程中不会出现丢失精度或取整等错误。

考虑这个要求

  1. 如果你给定一个数,把它转化为罗马数字表示,然后再转换回阿拉伯数字表示,你所得到的应该是最初给定的那个数。因此,对于 1..3999 中的nfromRoman(toRoman(n)) == n 总成立。

例 13.5. 以 toRoman 测试 fromRoman 的输出


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 我想提一下,这里的 integer 并不是一个 Python 关键字,而只是没有什么特别的变量名。
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[11]但是这样将会违背一条基本规则:每个测试用例只回答一个问题。试想一下,你将这个测试并入到完备性测试中,然后遇到了测试失败。你还需要进一步分析以便判定测试用例的哪部分出了问题。如果你需要分析方能找出问题所在,无疑你的测试用例在设计上出了问题。
2 这有一个和前面相似的情况:尽管 “你知道toRoman 总是返回大写字母,你还是需要把返回值显式地转换成大写字母后再传递给只接受大写的 fromRoman 进行测试。为什么?因为 toRoman 只返回大写字母是一个独立的需求。如果你改变了这个需求,例如改成总是返回小写字母,那么 testToRomanCase 测试用例也应作出调整,但这个测试用例应该仍能通过。这是另外一个基本规则:每个测试用例必须可以与其他测试用例隔离工作,每个测试用例是一个“孤岛”。
3 注意你并没有使用 fromRoman 的返回值。这是一个有效的 Python 语法:如果一个函数返回一个值,但没有被使用,Python 会直接把这个返回值扔掉。这正是你所希望的,这个测试用例并不对返回值进行测试,只是测试 fromRoman 接受大写字母而不引发异常。
4 这行有点复杂,但是它与 ToRomanBadInputFromRomanBadInput 测试很相似。 你在测试以特定值 (numeral.lower(),循环中目前罗马数字的小写版) 调用特定函数 (roman.fromRoman) 会确实引发特定的异常 (roman.InvalidRomanNumeralError)。如果 (在循环中的每一次) 确实如此,测试通过;如果有一次不是这样 (比如引发另外的异常或者不引发异常),测试失败。

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

Footnotes

[11] 除了诱惑什么我都能抗拒。 (I can resist everything except temptation.)”――Oscar Wilde