# 15.2. 应对需求变化
尽管你竭尽努力地分析你的客户,并点灯熬油地提炼出精确的需求,但需求还是会是不断变化。大部分客户在看到产品前不知道他们想要什么。即便知道,也不擅于精确表述出他们的有效需求。即便能表述出来,他们在下一个版本一定会要求更多的功能。因此你需要做好更新测试用例的准备以应对需求的改变。
假设你想要扩展罗马数字转换函数的范围。还记得[没有哪个字符可以重复三遍以上](../unit_testing/diving_in.html "13.2. 深入")这条规则吗?呃,现在罗马人希望给这条规则来个例外,用连续出现 4 个 `M` 字符来表示 `4000`。如果这样改了,你就可以把转换范围从 `1..3999` 扩展到 `1..4999`。但你先要对测试用例进行修改。
## 例 15.6. 修改测试用例以适应新需求 (`romantest71.py`)
这个文件可以在例子目录下的 `py/roman/stage7/` 目录中找到。
如果您还没有下载本书附带的样例程序, 可以 [下载本程序和其他样例程序](http://www.woodpecker.org.cn/diveintopython/download/diveintopython-exampleszh-cn-5.4b.zip "Download example scripts")。
```
import roman71
import unittest
class KnownValues(unittest.TestCase):
knownValues = ( (1, 'I'),
(2, 'II'),
(3, 'III'),
(4, 'IV'),
(5, 'V'),
(6, 'VI'),
(7, 'VII'),
(8, 'VIII'),
(9, 'IX'),
(10, 'X'),
(50, 'L'),
(100, 'C'),
(500, 'D'),
(1000, 'M'),
(31, 'XXXI'),
(148, 'CXLVIII'),
(294, 'CCXCIV'),
(312, 'CCCXII'),
(421, 'CDXXI'),
(528, 'DXXVIII'),
(621, 'DCXXI'),
(782, 'DCCLXXXII'),
(870, 'DCCCLXX'),
(941, 'CMXLI'),
(1043, 'MXLIII'),
(1110, 'MCX'),
(1226, 'MCCXXVI'),
(1301, 'MCCCI'),
(1485, 'MCDLXXXV'),
(1509, 'MDIX'),
(1607, 'MDCVII'),
(1754, 'MDCCLIV'),
(1832, 'MDCCCXXXII'),
(1993, 'MCMXCIII'),
(2074, 'MMLXXIV'),
(2152, 'MMCLII'),
(2212, 'MMCCXII'),
(2343, 'MMCCCXLIII'),
(2499, 'MMCDXCIX'),
(2574, 'MMDLXXIV'),
(2646, 'MMDCXLVI'),
(2723, 'MMDCCXXIII'),
(2892, 'MMDCCCXCII'),
(2975, 'MMCMLXXV'),
(3051, 'MMMLI'),
(3185, 'MMMCLXXXV'),
(3250, 'MMMCCL'),
(3313, 'MMMCCCXIII'),
(3408, 'MMMCDVIII'),
(3501, 'MMMDI'),
(3610, 'MMMDCX'),
(3743, 'MMMDCCXLIII'),
(3844, 'MMMDCCCXLIV'),
(3888, 'MMMDCCCLXXXVIII'),
(3940, 'MMMCMXL'),
(3999, 'MMMCMXCIX'),
(4000, 'MMMM'),
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX'))
def testToRomanKnownValues(self):
"""toRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.toRoman(integer)
self.assertEqual(numeral, result)
def testFromRomanKnownValues(self):
"""fromRoman should give known result with known input"""
for integer, numeral in self.knownValues:
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)
class ToRomanBadInput(unittest.TestCase):
def testTooLarge(self):
"""toRoman should fail with large input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 5000)
def testZero(self):
"""toRoman should fail with 0 input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0)
def testNegative(self):
"""toRoman should fail with negative input"""
self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -1)
def testNonInteger(self):
"""toRoman should fail with non-integer input"""
self.assertRaises(roman71.NotIntegerError, roman71.toRoman, 0.5)
class FromRomanBadInput(unittest.TestCase):
def testTooManyRepeatedNumerals(self):
"""fromRoman should fail with too many repeated numerals"""
for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'):
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)
def testRepeatedPairs(self):
"""fromRoman should fail with repeated pairs of numerals"""
for s in ('CMCM', 'CDCD', 'XCXC', 'XLXL', 'IXIX', 'IVIV'):
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)
def testMalformedAntecedent(self):
"""fromRoman should fail with malformed antecedents"""
for s in ('IIMXCC', 'VX', 'DCM', 'CMM', 'IXIV',
'MCMC', 'XCX', 'IVI', 'LM', 'LD', 'LC'):
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)
def testBlank(self):
"""fromRoman should fail with blank string"""
self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, "")
class SanityCheck(unittest.TestCase):
def testSanity(self):
"""fromRoman(toRoman(n))==n for all n"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
result = roman71.fromRoman(numeral)
self.assertEqual(integer, result)
class CaseCheck(unittest.TestCase):
def testToRomanCase(self):
"""toRoman should always return uppercase"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
self.assertEqual(numeral, numeral.upper())
def testFromRomanCase(self):
"""fromRoman should only accept uppercase input"""
for integer in range(1, 5000):
numeral = roman71.toRoman(integer)
roman71.fromRoman(numeral.upper())
self.assertRaises(roman71.InvalidRomanNumeralError,
roman71.fromRoman, numeral.lower())
if __name__ == "__main__":
unittest.main()
```
| | |
| --- | --- |
| \[1\] | 原来的已知值没有改变 (它们仍然是合理的测试值) 但你需要添加几个大于 `4000` 的值。这里我添加了 `4000` (最短的一个),`4500` (次短的一个),`4888` (最长的一个) 和 `4999` (值最大的一个)。 |
| \[2\] | “最大输入”的定义改变了。以前是以 `4000` 调用 `toRoman` 并期待一个错误;而现在 `4000-4999` 成为了有效输入,需要将这个最大输入提升至 `5000`。 |
| \[3\] | “过多字符重复” 的定义也改变了。这个测试以前是以 `'MMMM'` 调用 `fromRoman` 并期待一个错误;而现在 `MMMM` 被认为是一个有效的罗马数字表示,需要将这个“过多字符重复”改为 `'MMMMM'`。 |
| \[4\] | 完备测试和大小写测试原来在 `1` 到 `3999` 范围内循环。现在范围扩展了,这个 `for` 循环需要将范围也提升至 `4999`。 |
现在你的测试用例和新需求保持一致了,但是你的程序代码还没有,因此几个测试用例的失败是意料之中的事。
## 例 15.7. 用 `romantest71.py` 测试 `roman71.py` 的结果
```
fromRoman should only accept uppercase input ... ERROR toRoman should always return uppercase ... ERROR
fromRoman should fail with blank string ... ok
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 ... ERROR toRoman should give known result with known input ... ERROR fromRoman(toRoman(n))==n for all n ... ERROR 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
```
| | |
| --- | --- |
| \[1\] | 我们的大小写检查是因为循环范围是 `1` 到 `4999`,而 `toRoman` 只接受 `1` 到 `3999` 之间的数,因此测试循环到 `4000` 就会失败。 |
| \[2\] | `fromRoman` 的已知值测试在遇到 `'MMMM'` 就会失败,因为 `fromRoman` 还认为这是一个无效的罗马数字表示。 |
| \[3\] | `toRoman` 的已知值测试在遇到 `4000` 就会失败,因为 `toRoman` 仍旧认为这超出了有效值范围。 |
| \[4\] | 完备测试在遇到 `4000` 也会失败,因为 `toRoman` 也会认为这超出了有效值范围。 |
```
======================================================================
ERROR: fromRoman should only accept uppercase input
---------------------------------------------------------------------- Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 161, in testFromRomanCase
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999) ======================================================================
ERROR: toRoman should always return uppercase
---------------------------------------------------------------------- Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 155, in testToRomanCase
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999) ======================================================================
ERROR: fromRoman should give known result with known input
---------------------------------------------------------------------- Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 102, in testFromRomanKnownValues
result = roman71.fromRoman(numeral)
File "roman71.py", line 47, in fromRoman
raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
InvalidRomanNumeralError: Invalid Roman numeral: MMMM ======================================================================
ERROR: toRoman should give known result with known input
---------------------------------------------------------------------- Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 96, in testToRomanKnownValues
result = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999) ======================================================================
ERROR: fromRoman(toRoman(n))==n for all n
---------------------------------------------------------------------- Traceback (most recent call last):
File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 147, in testSanity
numeral = roman71.toRoman(integer)
File "roman71.py", line 28, in toRoman
raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999) ----------------------------------------------------------------------
Ran 13 tests in 2.213s
FAILED (errors=5)
```
既然新的需求导致了测试用例的失败,你该考虑修改代码以便它能再次通过测试用例。(在你开始编写单元测试时要习惯一件事:被测试代码永远不会在编写测试用例“之前”编写。正因为如此,你还有一些工作要做,一旦可以通过所有的测试用例,停止编码。)
## 例 15.8. 为新的需求编写代码 (`roman72.py`)
这个文件可以在例子目录下的 `py/roman/stage7/` 目录中找到。
```
"""Convert to and from Roman numerals"""
import re
#Define exceptions
class RomanError(Exception): pass
class OutOfRangeError(RomanError): pass
class NotIntegerError(RomanError): pass
class InvalidRomanNumeralError(RomanError): pass
#Define digit mapping
romanNumeralMap = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))
def toRoman(n):
"""convert integer to Roman numeral"""
if not (0 < n < 5000):
raise OutOfRangeError, "number out of range (must be 1..4999)"
if int(n) <> n:
raise NotIntegerError, "non-integers can not be converted"
result = ""
for numeral, integer in romanNumeralMap:
while n >= integer:
result += numeral
n -= integer
return result
#Define pattern to detect valid Roman numerals
romanNumeralPattern = '^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
def fromRoman(s):
"""convert Roman numeral to integer"""
if not s:
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\] | `toRoman` 只需要在取值范围检查一处做个小改动。将原来的 `0 < n < 4000`,更改为现在的检查 `0 < n < 5000`。你还要更改你 `raise` 的错误信息以反映接受新取值范围 (`1..4999` 而不再是 `1..3999`)。你不需要改变函数的其他部分,它们已经适用于新的情况。(它们会欣然地为新的 1000 添加 `'M'`,以 `4000` 为例,函数会返回 `'MMMM'` 。之前没能这样做是因为到范围检查时就被停了下来。) |
| \[2\] | 你对 `fromRoman` 也不需要做过多的修改。唯一的修改就在 `romanNumeralPattern`:如果你注意的话,你会发现你只需在正则表达式的第一部分增加一个可选的 `M` 。这就允许最多 4 个 `M` 字符而不再是 3 个,意味着你允许代表 `4999` 而不只是 `3999` 的罗马数字。`fromRoman` 函数本身是普遍适用的,它并不在意字符被多少次的重复,只是根据重复的罗马字符对应的数值进行累加。以前没能处理 `'MMMM'` 是因为你通过正则表达式的检查强行停止了。 |
你可能会怀疑只需这两处小改动。嘿,不相信我的话,你自己看看吧:
## 例 15.9. 用 `romantest72.py` 测试 `roman72.py` 的结果
```
fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok
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 3.685s
OK
```
| | |
| --- | --- |
| \[1\] | 所有的测试用例都通过了,停止编写代码。 |
全面的单元测试意味着不必依赖于程序员的一面之词:“相信我!”
- 版权信息
- 第 1 章 安装 Python
- 1.1. 哪一种 Python 适合您?
- 1.2. Windows 上的 Python
- 1.3. Mac OS X 上的 Python
- 1.4. Mac OS 9 上的 Python
- 1.5. RedHat Linux 上的 Python
- 1.6. Debian GNU/Linux 上的 Python
- 1.7. 从源代码安装 Python
- 1.8. 使用 Python 的交互 Shell
- 1.9. 小结
- 第 2 章 第一个 Python 程序
- 2.1. 概览
- 2.2. 函数声明
- 2.3. 文档化函数
- 2.4. 万物皆对象
- 2.5. 代码缩进
- 2.6. 测试模块
- 第 3 章 内置数据类型
- 3.1. Dictionary 介绍
- 3.2. List 介绍
- 3.3. Tuple 介绍
- 3.4. 变量声明
- 3.5. 格式化字符串
- 3.6. 映射 list
- 3.7. 连接 list 与分割字符串
- 3.8. 小结
- 第 4 章 自省的威力
- 4.1. 概览
- 4.2. 使用可选参数和命名参数
- 4.3. 使用 type、str、dir 和其它内置函数
- 4.4. 通过 getattr 获取对象引用
- 4.5. 过滤列表
- 4.6. and 和 or 的特殊性质
- 4.7. 使用 lambda 函数
- 4.8. 全部放在一起
- 4.9. 小结
- 第 5 章 对象和面向对象
- 5.1. 概览
- 5.2. 使用 from _module_ import 导入模块
- 5.3. 类的定义
- 5.4. 类的实例化
- 5.5. 探索 UserDict:一个封装类
- 5.6. 专用类方法
- 5.7. 高级专用类方法
- 5.8. 类属性介绍
- 5.9. 私有函数
- 5.10. 小结
- 第 6 章 异常和文件处理
- 6.1. 异常处理
- 6.2. 与文件对象共事
- 6.3. for 循环
- 6.4. 使用 `sys.modules`
- 6.5. 与目录共事
- 6.6. 全部放在一起
- 6.7. 小结
- 第 7 章 正则表达式
- 7.1. 概览
- 7.2. 个案研究:街道地址
- 7.3. 个案研究:罗马字母
- 7.4. 使用 {n,m} 语法
- 7.5. 松散正则表达式
- 7.6. 个案研究:解析电话号码
- 7.7. 小结
- 第 8 章 HTML 处理
- 8.1. 概览
- 8.2. sgmllib.py 介绍
- 8.3. 从 HTML 文档中提取数据
- 8.4. BaseHTMLProcessor.py 介绍
- 8.5. locals 和 globals
- 8.6. 基于 dictionary 的字符串格式化
- 8.7. 给属性值加引号
- 8.8. dialect.py 介绍
- 8.9. 全部放在一起
- 8.10. 小结
- 第 9 章 XML 处理
- 9.1. 概览
- 9.2. 包
- 9.3. XML 解析
- 9.4. Unicode
- 9.5. 搜索元素
- 9.6. 访问元素属性
- 9.7. Segue [9]
- 第 10 章 脚本和流
- 10.1. 抽象输入源
- 10.2. 标准输入、输出和错误
- 10.3. 查询缓冲节点
- 10.4. 查找节点的直接子节点
- 10.5. 根据节点类型创建不同的处理器
- 10.6. 处理命令行参数
- 10.7. 全部放在一起
- 10.8. 小结
- 第 11 章 HTTP Web 服务
- 11.1. 概览
- 11.2. 避免通过 HTTP 重复地获取数据
- 11.3. HTTP 的特性
- 11.4. 调试 HTTP web 服务
- 11.5. 设置 User-Agent
- 11.6. 处理 Last-Modified 和 ETag
- 11.7. 处理重定向
- 11.8. 处理压缩数据
- 11.9. 全部放在一起
- 11.10. 小结
- 第 12 章 SOAP Web 服务
- 12.1. 概览
- 12.2. 安装 SOAP 库
- 12.3. 步入 SOAP
- 12.4. SOAP 网络服务查错
- 12.5. WSDL 介绍
- 12.6. 以 WSDL 进行 SOAP 内省
- 12.7. 搜索 Google
- 12.8. SOAP 网络服务故障排除
- 12.9. 小结
- 第 13 章 单元测试
- 13.1. 罗马数字程序介绍 II
- 13.2. 深入
- 13.3. romantest.py 介绍
- 13.4. 正面测试 (Testing for success)
- 13.5. 负面测试 (Testing for failure)
- 13.6. 完备性检测 (Testing for sanity)
- 第 14 章 测试优先编程
- 14.1. roman.py, 第 1 阶段
- 14.2. roman.py, 第 2 阶段
- 14.3. roman.py, 第 3 阶段
- 14.4. roman.py, 第 4 阶段
- 14.5. roman.py, 第 5 阶段
- 第 15 章 重构
- 15.1. 处理 bugs
- 15.2. 应对需求变化
- 15.3. 重构
- 15.4. 后记
- 15.5. 小结
- 第 16 章 函数编程
- 16.1. 概览
- 16.2. 找到路径
- 16.3. 重识列表过滤
- 16.4. 重识列表映射
- 16.5. 数据中心思想编程
- 16.6. 动态导入模块
- 16.7. 全部放在一起
- 16.8. 小结
- 第 17 章 动态函数
- 17.1. 概览
- 17.2. plural.py, 第 1 阶段
- 17.3. plural.py, 第 2 阶段
- 17.4. plural.py, 第 3 阶段
- 17.5. plural.py, 第 4 阶段
- 17.6. plural.py, 第 5 阶段
- 17.7. plural.py, 第 6 阶段
- 17.8. 小结
- 第 18 章 性能优化
- 18.1. 概览
- 18.2. 使用 timeit 模块
- 18.3. 优化正则表达式
- 18.4. 优化字典查找
- 18.5. 优化列表操作
- 18.6. 优化字符串操作
- 18.7. 小结
- 附录 A. 进一步阅读
- 附录 B. 五分钟回顾
- 附录 C. 技巧和窍门
- 附录 D. 示例清单
- 附录 E. 修订历史
- 附录 F. 关于本书
- 附录 G. GNU Free Documentation License
- G.0. Preamble
- G.1. Applicability and definitions
- G.2. Verbatim copying
- G.3. Copying in quantity
- G.4. Modifications
- G.5. Combining documents
- G.6. Collections of documents
- G.7. Aggregation with independent works
- G.8. Translation
- G.9. Termination
- G.10. Future revisions of this license
- G.11. How to use this License for your documents
- 附录 H. GNU 自由文档协议
- H.0. 序
- H.1. 适用范围和定义
- H.2. 原样复制
- H.3. 大量复制
- H.4. 修改
- H.5. 合并文档
- H.6. 文档合集
- H.7. 独立著作聚集
- H.8. 翻译
- H.9. 终止协议
- H.10. 协议将来的修订
- H.11. 如何为你的文档使用本协议
- 附录 I. Python license
- I.A. History of the software
- I.B. Terms and conditions for accessing or otherwise using Python
- 附录 J. Python 协议
- J.0. 关于译文的声明
- J.A. 软件的历史
- J.B. 使用 Python 的条款和条件