预计阅读本页时间:-
6.3 模拟(mocking)
mock对象即模拟对象,用来通过某种特殊和可控的方式模拟真实应用程序对象的行为。在创建精确地描述测试代码的状态的环境时,它们非常有用。
如果正在开发一个HTTP客户端,要想部署HTTP服务器并测试所有场景,令其返回所有可能值,几乎是不可能的(至少会非常复杂)。此外,测试所有失败场景也是极其困难的。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
一种更简单的方式是创建一组根据这些特定场景进行建模的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错误,或者发生网络问题。我们可以确定代码返回的是正确的值,或在每种情况下抛出正确的异常,总之确保代码总是按照预期行事。