第 15 章 重构

15.1. 处理 bugs

尽管你很努力地编写全面的单元测试,但是 bug 还是会出现。我所说的 “bug” 是什么呢?Bug 是你还没有编写的测试用例。

例 15.1. 关于 Bug

>>> import roman5
>>> roman5.fromRoman("") 1
0
1 在前面的章节中你注意到一个空字符串会匹配上那个检查罗马数字有效性的正则表达式了吗?对于最终版本中的正则表达式这一点仍然没有改变。这就是一个 Bug ,你希望空字符串能够像其他无效的罗马数字表示一样引发 InvalidRomanNumeralError 异常。

在重现这个 Bug 并修改它之前你应该编写一个会失败的测试用例来说明它。

例 15.2. 测试 bug (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 异常。难点在于找出 Bug,既然你已经知道它了,测试就简单了。

因为你的代码存在一个 Bug,并且你编写了测试这个 Bug 的测试用例,所以测试用例将会失败:

例 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)

现在 你可以修改这个 Bug了。

例 15.4. 修改 Bug (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 空字符串测试用例现在通过了,说明 Bug 被修正了。
2 所有其他测试用例依然通过,证明这个 Bug 修正没有影响到其他部分。不需要再编程了。

这样编程,并没有令 Bug 修正变得简单。简单的 Bug (就像这一个) 需要简单的测试用例,复杂 Bug 则需要复杂的测试用例。以测试为核心的氛围好像 延长了修正 Bug 的时间,因为你需要先贴切地描述出 Bug (编写测试用例) 然后才去修正它。如果测试用例没能正确通过,你需要思量这个修改错了还是测试用例本身出现了 Bug。无论如何,从长远上讲,这样在测试代码和代码之间的反复是值得的,因为这样会使 Bug 在第一时间就被修正的可能性大大提高。而且不论如何更改,你都可以轻易地重新运行所有 测试用例,新代码破坏老代码的机会也变得微乎其微。今天的单元测试就是明天的回归测试 (regression test)。