6.3 模拟(mocking)
mock对象即模拟对象,用来通过某种特殊和可控的方式模拟真实应用程序对象的行为。在创建精确地描述测试代码的状态的环境时,它们非常有用。
如果正在开发一个HTTP客户端,要想部署HTTP服务器并测试所有场景,令其返回所有可能值,几乎是不可能的(至少会非常复杂)。此外,测试所有失败场景也是极其困难的。
一种更简单的方式是创建一组根据这些特定场景进行建模的mock对象,并利用它们作为测试环境对代码进行测试。
Python标准库中用来创建mock对象的库名为mock(https://pypi.python.org/pypi/mock/ )。从Python 3.3开始,它被命名为unit.mock,合并到Python标准库。因此可以使用下面的代码片段:
try: from unittest import mock except ImportError: import mock
要保持Python 3.3和之前版本之间的向后兼容。
它使用起来也非常简单,如示例6.6所示。
示例6.6 mock的基本用法
>>> import mock >>> m = mock.Mock() >>> m.some_method.return_value = 42 >>> m.some_method() 42 >>> def print_hello(): ... print("hello world!") ... >>> m.some_method.side_effect = print_hello >>> m.some_method() hello world! >>> def print_hello(): ... print("hello world!") ... return 43 ... >>> m.some_method.side_effect = print_hello >>> m.some_method() hello world! 43 >>> m.some_method.call_count 3
即使只使用这一组功能,也应该可以模拟许多内部对象以用于不同的数据场景中。
模拟使用动作/断言模式,也就是说一旦测试运行,必须确保模拟的动作被正确地执行,如示例6.7所示。
示例6.7 确认方法调用
>>> import mock >>> m = mock.Mock() >>> m.some_method('foo', 'bar') <Mock name='mock.some_method()' id='26144272'> >>> m.some_method.assert_called_once_with('foo', 'bar') >>> m.some_method.assert_called_once_with('foo', mock.ANY) >>> m.some_method.assert_called_once_with('foo', 'baz') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python2.7/dist-packages/mock.py", line 846, in assert_called_once_with return self.assert_called_with(*args, **kwargs) File "/usr/lib/python2.7/dist-packages/mock.py", line 835, in assert_called_with raise AssertionError(msg) AssertionError: Expected call: some_method('foo', 'baz') Actual call: some_method('foo', 'bar')
显然,很容易传一个mock对象到代码的任何部分,并在其后检查代码是否按其期望的传入参数被调用。如果不知道该传入何种参数,可以使用mock.ANY作为参数值传入,它将会匹配传递给mock方法的任何参数。
有时可能需要来自外部模块的函数、方法或对象。mock库为此提供了一组补丁函数。
示例6.8 使用mock.patch
>>> import mock >>> import os >>> def fake_os_unlink(path): ... raise IOError("Testing!") ... >>> with mock.patch('os.unlink', fake_os_unlink): ... os.unlink('foobar') ... Traceback (most recent call last): File "<stdin>", line 2, in <module> File "<stdin>", line 2, in fake_os_unlink IOError: Testing!
通过mock.pach方法,可以修改外部代码的任何部分,使其按照需要的方式对软件进行各种条件下的测试,如示例6.9所示。
示例6.9 使用mock.patch测试一组行为
import requests import unittest import mock class WhereIsPythonError(Exception): pass def is_python_still_a_programming_language(): try: r = requests.get("http://python.org") except IOError: pass else: if r.status_code == 200: return 'Python is a programming language' in r.content raise WhereIsPythonError("Something bad happened") def get_fake_get(status_code, content): m = mock.Mock() m.status_code = status_code m.content = content def fake_get(url): return m return fake_get def raise_get(url): raise IOError("Unable to fetch url %s" % url) class TestPython(unittest.TestCase): @mock.patch('requests.get', get_fake_get( 200, 'Python is a programming language for sure')) def test_python_is(self): self.assertTrue(is_python_still_a_programming_language()) @mock.patch('requests.get', get_fake_get( 200, 'Python is no more a programming language')) def test_python_is_not(self): self.assertFalse(is_python_still_a_programming_language()) @mock.patch('requests.get', get_fake_get( 404, 'Whatever')) def test_bad_status_code(self): self.assertRaises(WhereIsPythonError, is_python_still_a_programming_language) @mock.patch('requests.get', raise_get) def test_ioerror(self): self.assertRaises(WhereIsPythonError, is_python_still_a_programming_language)
示例6.9使用了mock.patch的装饰器版本,这并不改变它的行为,但当需要在整个测试函数的上下文内使用模拟时这会更方便。
使用模拟可以很方便地模拟任何问题,如Web服务器返回404错误,或者发生网络问题。我们可以确定代码返回的是正确的值,或在每种情况下抛出正确的异常,总之确保代码总是按照预期行事。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论