第 15 章. 重构

15.1. 处理错误

尽管您尽最大努力编写了全面的单元测试,但错误仍然会发生。我所说的“错误”是什么意思?错误是指您尚未编写的测试用例。

示例 15.1. 错误

>>> import roman5
>>> roman5.fromRoman("") 1
0
1 还记得在上一节中,您一直看到空字符串会匹配您用来检查有效罗马数字的正则表达式吗?事实证明,对于最终版本的正则表达式来说,这仍然是正确的。这是一个错误;您希望空字符串像任何其他不代表有效罗马数字的字符序列一样引发 InvalidRomanNumeralError 异常。

在重现错误之后以及修复错误之前,您应该编写一个失败的测试用例,从而说明该错误。

示例 15.2. 测试错误 (romantest61.py)


class FromRomanBadInput(unittest.TestCase):                                      

    # previous test cases omitted for clarity (they haven't changed)

    def testBlank(self):
        """fromRoman should fail with blank string"""
        self.assertRaises(roman.InvalidRomanNumeralError, roman.fromRoman, "") 1
1 这里的内容非常简单。使用空字符串调用 fromRoman 并确保它引发 InvalidRomanNumeralError 异常。困难的部分是找到错误;既然您已经知道了错误,那么测试它就很容易了。

由于您的代码存在错误,并且您现在有了一个测试该错误的测试用例,因此该测试用例将失败

示例 15.3. romantest61.py 针对 roman61.py 的输出

fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... FAIL
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok

======================================================================
FAIL: fromRoman should fail with blank string
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage6\romantest61.py", line 137, in testBlank
    self.assertRaises(roman61.InvalidRomanNumeralError, roman61.fromRoman, "")
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
----------------------------------------------------------------------
Ran 13 tests in 2.864s

FAILED (failures=1)

现在您可以修复错误了。

示例 15.4. 修复错误 (roman62.py)

此文件位于示例目录的 py/roman/stage6/ 中。


def fromRoman(s):
    """convert Roman numeral to integer"""
    if not s: 1
        raise InvalidRomanNumeralError, 'Input can not be blank'
    if not re.search(romanNumeralPattern, s):
        raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s

    result = 0
    index = 0
    for numeral, integer in romanNumeralMap:
        while s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    return result
1 只需要两行代码:对空字符串的显式检查和 raise 语句。

示例 15.5. romantest62.py 针对 roman62.py 的输出

fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok 1
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 13 tests in 2.834s

OK 2
1 现在,空字符串测试用例通过了,因此错误已修复。
2 所有其他测试用例仍然通过,这意味着此错误修复没有破坏任何其他内容。停止编码。

以这种方式编码并不能使修复错误变得更容易。简单的错误(如这个错误)需要简单的测试用例;复杂的错误将需要复杂的测试用例。在以测试为中心的环境中,修复错误似乎需要更长的时间,因为您需要在代码中准确地阐述错误是什么(编写测试用例),然后修复错误本身。然后,如果测试用例没有立即通过,您需要弄清楚是修复错误了,还是测试用例本身存在错误。但是,从长远来看,测试代码和被测代码之间的这种来回交互是值得的,因为它可以提高第一次就正确修复错误的可能性。此外,由于您可以轻松地重新运行所有测试用例以及您的新测试用例,因此在修复新代码时,您不太可能破坏旧代码。今天的单元测试就是明天的回归测试。