Logo
Section 1 / 4

深入探究

先写失败测试再修复空字符串错误,并使用省略索引的字符串格式化

导入

深入探究

先写失败测试再修复空字符串错误,并使用省略索引的字符串格式化

无论你是否喜欢,错误总是会发生。尽管你尽了最大努力编写全面的[单元测试],错误依然会出现。我所说的“错误”是什么意思?一个错误就是你尚未编写的测试用例。

1import roman7 2print(roman7.from_roman('')) #① 3# 0

① 这是一个错误。空字符串应当引发 InvalidRomanNumeralError 异常,就像任何不表示有效罗马数字的字符序列一样。

在复现该错误之后,在修复之前,你应该编写一个失败的测试用例,以明确展示这个错误。

1import unittest 2 3class FromRomanBadInput(unittest.TestCase): 4 #. 5 #. 6 #. 7 def testBlank(self): 8 '''from_roman 应当对空字符串抛出异常''' 9 self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '') #①

① 这里逻辑非常简单:传入一个空字符串调用 from_roman(),并确保它会抛出 InvalidRomanNumeralError 异常。发现这个错误是困难的部分;现在既然你知道了问题所在,测试它就变得很简单了。

由于你的代码存在缺陷,而你现在有了一个测试这个缺陷的用例,因此该测试将失败:

1you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v 2from_roman should fail with blank string ... FAIL 3from_roman should fail with malformed antecedents ... ok 4from_roman should fail with repeated pairs of numerals ... ok 5from_roman should fail with too many repeated numerals ... ok 6from_roman should give known result with known input ... ok 7to_roman should give known result with known input ... ok 8from_roman(to_roman(n))==n for all n ... ok 9to_roman should fail with negative input ... ok 10to_roman should fail with non-integer input ... ok 11to_roman should fail with large input ... ok 12to_roman should fail with 0 input ... ok 13 14====================================================================== 15FAIL: from_roman should fail with blank string 16---------------------------------------------------------------------- 17Traceback (most recent call last): 18 File "romantest8.py", line 117, in test_blank 19 self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '') 20AssertionError: InvalidRomanNumeralError not raised by from_roman 21 22---------------------------------------------------------------------- 23Ran 11 tests in 0.171s 24 25FAILED (failures=1)

现在你可以修复这个错误。

1def from_roman(s): 2 '''将罗马数字转换为整数''' 3 if not s: #① 4 raise InvalidRomanNumeralError('输入不能为空') 5 if not re.search(romanNumeralPattern, s): 6 raise InvalidRomanNumeralError('无效的罗马数字: {}'.format(s)) #② 7 8 result = 0 9 index = 0 10 for numeral, integer in romanNumeralMap: 11 while s[index:index+len(numeral)] == numeral: 12 result += integer 13 index += len(numeral) 14 return result

① 只需要两行代码:显式检查空字符串,并抛出异常。

② 我可能之前在这本书中从未提过这一点,所以这里作为你关于[字符串格式化]的最后课程。从 Python 3.1 开始,你在格式化字符串时可以省略位置索引编号。也就是说,不再需要用 {0} 来引用 format() 方法的第一个参数,可以直接使用 {},Python 会自动填充正确的索引位置。这种方法适用于任意数量的参数:第一个 {} 对应 {0},第二个 {} 对应 {1},依此类推。

1you@localhost:~/diveintopython3/examples$ python3 romantest8.py -v 2from_roman should fail with blank string ... ok ① 3from_roman should fail with malformed antecedents ... ok 4from_roman should fail with repeated pairs of numerals ... ok 5from_roman should fail with too many repeated numerals ... ok 6from_roman should give known result with known input ... ok 7to_roman should give known result with known input ... ok 8from_roman(to_roman(n))==n for all n ... ok 9to_roman should fail with negative input ... ok 10to_roman should fail with non-integer input ... ok 11to_roman should fail with large input ... ok 12to_roman should fail with 0 input ... ok 13 14---------------------------------------------------------------------- 15Ran 11 tests in 0.156s 16 17OK ②

① 空字符串的测试现在通过了,说明错误已被修复。

② 其他所有测试用例仍然通过,这意味着这个修复没有破坏其他功能。停止编码。

这种方式编程并不会让修复错误变得更简单。简单的错误(如这个)只需要简单的测试用例;复杂的错误则需要复杂的测试用例。在以测试为中心的开发环境中,修复错误看起来可能更耗时,因为你需要在代码中精确描述错误(编写测试用例),然后才能修复错误本身。如果测试没有立即通过,你还得判断是修复错了,还是测试本身有问题。然而,从长远来看,这种在测试代码和被测代码之间来回调整的过程是值得的,因为它提高了首次正确修复错误的可能性。此外,由于你可以轻松地重新运行所有测试用例,包括新增的这一项,因此在修复新代码时,也大大降低了破坏旧代码的风险。今天的单元测试,就是明天的回归测试。

单元测试

应对需求变更