第 14 章 测试优先编程
Description: Python from novice to pro
Keywords: Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free

# 深入 Python :Dive Into Python 中文版 Python 从新手到专家 [Dip_5.4b_CPyUG_Release] | Find:

## 第 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
    class OutOfRangeError(RomanError): pass
    class NotIntegerError(RomanError): pass
    class InvalidRomanNumeralError(RomanError): pass

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

    def fromRoman(s):
        """convert Roman numeral to integer"""
        pass

| 这就是如何定义你自己的 Python 异常。异常 (Exception) 也是类，通过继承已有的异常，你可以创建自定义的异常。强烈建议 (但不是必须) 你继承 `Exception` 来定义自己的异常，因为它是所有内建异常的基类。这里我定义了 `RomanError` (从 `Exception` 继承而来) 作为我所有自定义异常的基类。这是一个风格问题，我也可以直接从 `Exception` 继承建立每一个自定义异常。
| `OutOfRangeError` 和 `NotIntegerError` 异常将会最终被用于 `toRoman` 以标示不同类型的无效输入，更具体而言就是 `ToRomanBadInput` 测试的那些。
| `InvalidRomanNumeralError` 将被最终用于 `fromRoman` 以标示无效输入，具体而言就是 `FromRomanBadInput`测试的那些。
| 在这一步中你只是想定义每个函数的 API ，而不想具体实现它们，因此你以 Python 关键字 `pass` 姑且带过。

重要的时刻到了 (请打起鼓来)：你终于要对这个简陋的小模块开始运行单元测试了。目前而言，每一个测试用例都应该失败。事实上，任何测试用例在此时通过，你都应该回头看看 `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
    ----------------------------------------------------------------------
    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
    ----------------------------------------------------------------------
    Ran 12 tests in 0.040s

    FAILED (failures=10, errors=2)

| 运行脚本将会执行 `unittest.main()`，由它来执行每个测试用例，也就是每个在 `romantest.py` 中定义的方法。对于每个测试用例，无论测试通过与否，都会输出这个方法的 `doc string`。意料之中，没有通过一个测试用例。
| 对于每个失败的测试用例，`unittest` 显示的跟踪信息告诉我们都发生了什么。就此处而言，调用 `assertRaises` (也称作 `failUnlessRaises`) 引发了一个 `AssertionError` 异常，因为期待 `toRoman` 所引发的 `OutOfRangeError` 异常没有出现。
| 在这些细节后面，`unittest` 给出了一个关于被执行测试的个数和花费时间的总结。
| 总而言之，由于至少一个测试用例没有通过，单元测试失败了。当某个测试用例没能通过时，`unittest` 会区分是失败 (failures) 还是错误 (errors)。失败是指调用 `assertXYZ` 方法，比如 `assertEqual` 或者 `assertRaises` 时，断言的情况没有发生或预期的异常没有被引发。而错误是指你测试的代码或单元测试本身发生了某种异常。例如：`testFromRomanCase` 方法 (“`fromRoman` 只接受大写输入”) 就是一个错误，因为调用 `numeral.upper()` 引发了一个 `AttributeError` 异常，因为 `toRoman` 的返回值不是期望的字符串类型。但是，`testZero` (“`toRoman` 应该在输入 0 时失败”) 是一个失败，因为调用 `fromRoman` 没有引发一个 `assertRaises` 期待的异常：`InvalidRomanNumeral`。