ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 14.1. `roman.py`, 第 1 阶段 到目前为止,单元测试已经完成,是时候开始编写被单元测试测试的代码了。你将分阶段地完成这个工作,因此开始时所有的单元测试都是失败的,但在逐步完成 `roman.py` 的同时你会看到它们一个个地通过测试。 ## 例 14.1. `roman1.py` 这个程序可以在例子目录下的 `py/roman/stage1/` 目录中找到。 如果您还没有下载本书附带的样例程序, 可以 [下载本程序和其他样例程序](http://www.woodpecker.org.cn/diveintopython/download/diveintopython-exampleszh-cn-5.4b.zip "Download example scripts")。 ``` """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 ``` | | | | --- | --- | | \[1\] | 这就是如何定义你自己的 Python 异常。异常 (Exception) 也是类,通过继承已有的异常,你可以创建自定义的异常。强烈建议 (但不是必须) 你继承 `Exception` 来定义自己的异常,因为它是所有内建异常的基类。这里我定义了 `RomanError` (从 `Exception` 继承而来) 作为我所有自定义异常的基类。这是一个风格问题,我也可以直接从 `Exception` 继承建立每一个自定义异常。 | | \[2\] | `OutOfRangeError` 和 `NotIntegerError` 异常将会最终被用于 `toRoman` 以标示不同类型的无效输入,更具体而言就是 [`ToRomanBadInput`](testing_for_failure.html#roman.tobadinput.example "例 13.3. 测试 toRoman 的无效输入") 测试的那些。 | | \[3\] | `InvalidRomanNumeralError` 将被最终用于 `fromRoman` 以标示无效输入,具体而言就是 [`FromRomanBadInput`](testing_for_failure.html#roman.frombadinput.example "例 13.4. 测试 fromRoman 的无效输入")测试的那些。 | | \[4\] | 在这一步中你只是想定义每个函数的 API ,而不想具体实现它们,因此你以 Python 关键字 [`pass`](../object_oriented_framework/defining_classes.html#fileinfo.class.simplest "例 5.3. 最简单的 Python 类") 姑且带过。 | 重要的时刻到了 (请打起鼓来):你终于要对这个简陋的小模块开始运行单元测试了。目前而言,每一个测试用例都应该失败。事实上,任何测试用例在此时通过,你都应该回头看看 `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) ``` | | | | --- | --- | | \[1\] | 运行脚本将会执行 `unittest.main()`,由它来执行每个测试用例,也就是每个在 `romantest.py` 中定义的方法。对于每个测试用例,无论测试通过与否,都会输出这个方法的 `doc string`。意料之中,没有通过一个测试用例。 | | \[2\] | 对于每个失败的测试用例,`unittest` 显示的跟踪信息告诉我们都发生了什么。就此处而言,调用 `assertRaises` (也称作 `failUnlessRaises`) 引发了一个 `AssertionError` 异常,因为期待 `toRoman` 所引发的 `OutOfRangeError` 异常没有出现。 | | \[3\] | 在这些细节后面,`unittest` 给出了一个关于被执行测试的个数和花费时间的总结。 | | \[4\] | 总而言之,由于至少一个测试用例没有通过,单元测试失败了。当某个测试用例没能通过时,`unittest` 会区分是失败 (failures) 还是错误 (errors)。失败是指调用 `assertXYZ` 方法,比如 `assertEqual` 或者 `assertRaises` 时,断言的情况没有发生或预期的异常没有被引发。而错误是指你测试的代码或单元测试本身发生了某种异常。例如:`testFromRomanCase` 方法 (“`fromRoman` 只接受大写输入”) 就是一个错误,因为调用 `numeral.upper()` 引发了一个 `AttributeError` 异常,因为 `toRoman` 的返回值不是期望的字符串类型。但是,`testZero` (“`toRoman` 应该在输入 0 时失败”) 是一个失败,因为调用 `fromRoman` 没有引发一个 `assertRaises` 期待的异常:`InvalidRomanNumeral`。 |