第 14 章:测试先行编程

14.1. roman.py,阶段 1

现在单元测试已经完成,是时候开始编写测试用例要测试的代码了。你将分阶段进行,以便你可以看到所有单元测试失败,然后在你填补 roman.py 中的空白时,看着它们一个接一个地通过。

示例 14.1. roman1.py

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

如果你还没有这样做,你可以 下载这个和本书中使用的其他示例

"""Convert to and from Roman numerals"""

#Define exceptions
class RomanError(Exception): pass                1
class OutOfRangeError(RomanError): pass          2
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass 3

def toRoman(n):
    """convert integer to Roman numeral"""
    pass                                         4

def fromRoman(s):
    """convert Roman numeral to integer"""
    pass
1 这就是你在 Python 中定义自定义异常的方式。异常是类,你可以通过继承现有异常来创建自己的异常。强烈建议(但不是必需)你继承 Exception,它是所有内置异常继承的基类。这里我定义了 RomanError(继承自 Exception)作为我所有其他自定义异常的基类。这是一个风格问题;我可以很容易地直接从 Exception 类继承每个单独的异常。
2 OutOfRangeErrorNotIntegerError 异常最终将由 toRoman 使用,以标记各种形式的无效输入,如 ToRomanBadInput 中所述。
3 InvalidRomanNumeralError 异常最终将由 fromRoman 使用,以标记无效输入,如 FromRomanBadInput 中所述。
4 在这个阶段,你需要定义每个函数的 API,但你还不希望对它们进行编码,因此你可以使用 Python 保留字 pass 将它们存根。

现在是重要时刻(请击鼓):你终于要针对这个粗陋的小模块运行单元测试了。在这一点上,每个测试用例都应该失败。事实上,如果在阶段 1 中有任何测试用例通过,你应该回到 romantest.py 并重新评估为什么你编写了一个如此无用的测试,以至于它可以通过什么都不做的函数。

使用 -v 命令行选项运行 romantest1.py,这将提供更详细的输出,以便你可以准确地看到每个测试用例运行时发生了什么。如果一切顺利,你的输出应该如下所示

示例 14.2. romantest1.py 针对 roman1.py 的输出

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

======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 154, in testFromRomanCase
    roman1.fromRoman(numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 148, in testToRomanCase
    self.assertEqual(numeral, numeral.upper())
AttributeError: 'None' object has no attribute 'upper'
======================================================================
FAIL: fromRoman should fail with malformed antecedents
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 133, in testMalformedAntecedent
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with repeated pairs of numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 127, in testRepeatedPairs
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should fail with too many repeated numerals
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 122, in testTooManyRepeatedNumerals
    self.assertRaises(roman1.InvalidRomanNumeralError, roman1.fromRoman, s)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: InvalidRomanNumeralError
======================================================================
FAIL: fromRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 99, in testFromRomanKnownValues
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 93, in testToRomanKnownValues
    self.assertEqual(numeral, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: I != None
======================================================================
FAIL: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 141, in testSanity
    self.assertEqual(integer, result)
  File "c:\python21\lib\unittest.py", line 273, in failUnlessEqual
    raise self.failureException, (msg or '%s != %s' % (first, second))
AssertionError: 1 != None
======================================================================
FAIL: toRoman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 116, in testNonInteger
    self.assertRaises(roman1.NotIntegerError, roman1.toRoman, 0.5)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: NotIntegerError
======================================================================
FAIL: toRoman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 112, in testNegative
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, -1)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 104, in testTooLarge
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 4000)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError
======================================================================
FAIL: toRoman should fail with 0 input                                 1
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage1\romantest1.py", line 108, in testZero
    self.assertRaises(roman1.OutOfRangeError, roman1.toRoman, 0)
  File "c:\python21\lib\unittest.py", line 266, in failUnlessRaises
    raise self.failureException, excName
AssertionError: OutOfRangeError                                        2
----------------------------------------------------------------------
Ran 12 tests in 0.040s                                                 3

FAILED (failures=10, errors=2)                                         4
1 运行脚本会运行 unittest.main(),它会运行每个测试用例,也就是说,在 romantest.py 中的每个类中定义的每个方法。对于每个测试用例,它都会打印出该方法的 文档字符串 以及该测试是通过还是失败。正如预期的那样,所有测试用例都没有通过。
2 对于每个失败的测试用例,unittest 都会显示跟踪信息,准确显示发生了什么。在这种情况下,对 assertRaises(也称为 failUnlessRaises)的调用引发了 AssertionError,因为它期望 toRoman 引发 OutOfRangeError,但它没有。
3 在详细信息之后,unittest 会显示执行了多少测试以及花费了多长时间的摘要。
4 总的来说,单元测试失败是因为至少有一个测试用例没有通过。当一个测试用例没有通过时,unittest 会区分失败和错误。失败是对 assertXYZ 方法(如 assertEqualassertRaises)的调用,因为断言的条件不正确或没有引发预期的异常而失败。错误是在你正在测试的代码或单元测试用例本身中引发的任何其他类型的异常。例如,testFromRomanCase 方法(“fromRoman 应该只接受大写输入”)是一个错误,因为对 numeral.upper() 的调用引发了 AttributeError 异常,因为 toRoman 应该返回一个字符串,但它没有。但是 testZero(“toRoman 应该在输入 0 时失败”)是一个失败,因为对 fromRoman 的调用没有引发 assertRaises 正在寻找的 InvalidRomanNumeral 异常。