模拟对于单元测试至关重要。
然而。
做对也很烦人。
如果我们不是 100% 完全清楚我们在模拟什么,我们只会将任何愚蠢的假设经典化为 实际上 不起作用的模拟对象。它们在不会崩溃的意义上起作用,但它们没有正确测试应用程序对象,因为它们重复了一些(错误的)假设。
当有疑问时,似乎我们必须谨慎行事。表现得好像我们正在打破一些测试先行的测试驱动开发规则。
笔记。我们并没有真正违反规则。然而,有些人会争辩说,测试驱动开发实际上意味着您采取的每项行动都应该由测试驱动。这是否包括早上喝咖啡或将显示器旋转到纵向模式?显然不是。技术峰值呢?
我们的立场是这样的。
- 尽早并经常设置峰值。
- 一旦您有理由相信这个疯狂的事情可能会奏效,您就可以通过测试将尖峰形式化。和模拟对象。
- 现在,您可以通过创建测试并围绕这些测试编写代码来编写应用程序的其余部分。
这里的重要部分是在您真正了解自己在做什么之前不要创建模拟。
图书范例
现在是棘手的部分:写一本书。
显然,每个示例都必须有某种单元测试。我为此大量使用 doctest。每个示例都在一个 doctest 测试字符串中。
章节的代码可能如下所示。
test_hello_world = '''
>>> print( 'hello world')
'hello world'
'''
test = { n:v for n,v in vars().items()
if n.startswith('test_') }
if name == 'main':
import doctest
doctest.testmod()
我们使用了 doctest 功能来查找分配给名为 __test__ 的变量的字典。该字典中的值是运行的测试,就好像它们是在模块、函数或类中找到的文档字符串一样。
这非常简单。规劝。例证。将示例复制并粘贴到脚本中以用于测试目的并在文本中展示。
直到我们获得外部服务。以及 RESTful API 请求等。这些都不好嘲笑。主要是因为模拟的单元测试非常没有信息。
假设我们正在撰写有关向 http://www.data.gov 发出 RESTful API 请求的文章。请求的结果非常有趣。发出请求的机制是 REST API 如何工作的一个重要示例。以及 CKAN 支持的网站的一般运作方式。
但是,如果我们用模拟 urllib 替换 urrlib.request,则单元测试相当于检查我们是否使用正确的参数调用了 urlopen()。对于许多实用的软件开发很重要,但对于下载本书相关代码的人来说也是统一的。
看来我有四个选择:
- 逆来顺受。并非所有示例都必须非常详细。
- 坚持使用尖峰版本。不要嘲笑事情。结果可能会有所不同,并且某些测试可能会在编辑器的桌面上失败。
- 跳过测试。
- 编写测试的多个版本:一个“使用真实互联网”版本和一个“使用公司防火墙代理拦截器”的版本,使用模拟并在任何地方工作。
到目前为止,我已经充分利用了前三个。第四个尴尬。我们最终得到这样的代码:
test_hello_world = '''
>>> print( 'hello world')
'hello world'
'''
test = { n:v for n,v in vars().items()
if n.startswith('test_') }
if name == 'main':
import doctest
doctest.testmod()
这不是用于企业软件开发目的的大量代码。事实上,它有点弱,因为它只测试 Happy Path。
但是对于一本书的例子来说,它似乎重于模拟模块而轻于感兴趣的主题。
事实上,我不希望任何人弄清楚它的说明性价值是什么,因为它只有 2 行相关代码包裹在成功模拟模块所需的 8 行样板文件中。
我对 unitest.mock 模块没有任何不满。它非常适合模拟模块;考虑到我们正在为被测单元的运行时环境做出什么样的痛苦改变,我认为样板文件是可以接受的。
这在解释时失败了。
我正在考虑如何处理其中一些更复杂的测试用例。过去,我跳过案例,并使用 doctest Ellipsis 功能来处理变体输出。我想我会继续这样做,因为模拟代码似乎对读者帮助不大,而且过于专注于证明所有代码完全正确的纯技术需求。