Python 的单元测试之 mock
Overview
mock 是一个用于单元测试的 Python 库,它使用 mock 模拟系统中如 class, method 等部分,并且断言它们是如何被调用的。在编写单元测试时,mock 非常适合模拟数据库,web 服务器等依赖外部的场景。本文是 mock 的入门篇,主要介绍 mock 的基本用法。
除了 mock 外,还有许多其它的 mocking 库,Python Mock Library Comparison 在用法上对这些库做了简单的比较,其中 OpenStack 单元测试广泛的使用了 mock 和 mox。
mock 的安装非常简便:
$ pip install mock
Mock Patching Methods
当使用 mock 模拟 methods 时,mock 会替换被模拟的 methods,并且记录调用详情。
>>> class Foo(object):
... def echo(self, *args):
... return "hello"
...
>>>
>>> foo = Foo()
>>> foo.echo = mock.MagicMock()
>>> foo.echo.return_value = "mock value"
>>>
>>> foo.echo()
'mock value'
>>> foo.echo(1, 2)
'mock value'
被模拟后,foo.echo 的类型是一个名为 mock.MagicMock 类,具有 assert_any_call, assert_called_once_with 等方法,其中 assert 类型的方法通常用于检验 foo.echo 是否被正确调用。
>>> type(foo.echo)
<class 'mock.MagicMock'>
>>> dir(foo.echo)
['assert_any_call', 'assert_called_once_with', 'assert_called_with',
'assert_has_calls', 'attach_mock', 'call_args', 'call_args_list',
'call_count', 'called', 'configure_mock', 'method_calls', 'mock_add_spec',
'mock_calls', 'reset_mock', 'return_value', 'side_effect']
断言 foo.echo 被调用的情况,其中 foo.echo 共被调用两次(见上)。
>>> foo.echo.called
True
>>> foo.echo.call_count
2
>>> foo.echo.mock_calls
[call(), call(1, 2)]
>>>
>>> foo.echo.assert_called_with(1, 2)
>>> foo.echo.assert_called_with(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.7/dist-packages/mock.py", line 844, in assert_called_with
raise AssertionError(msg)
AssertionError: Expected call: mock(1)
Actual call: mock(1, 2)
mock.Mock 和 mock.MagicMock 是两个常用的类,stackoverflow 有篇帖子 mock-vs-magicmock 专门讲述二者的区别:
- MagicMock 是 Mock 的之类
- MagicMock 额外实现了很多 magic 的方法
Mocking Classes
采用 mock 可方便的模拟 class,例如:
>>> def some_function():
... foo = Foo()
... return foo.echo()
...
>>> with mock.patch('__main__.Foo') as foo_mock:
... instance = foo_mock.return_value
... instance.echo.return_value = "mock result"
... result = some_function()
... assert result == "mock result"
...
>>> print some_function()
hello
值得注意的是,mock.patch 把模拟的效果限制在 with 作用域的范围内,所以 with 作用域之外的 some_function 的返回值依旧为 hello。
Setting Return Values and Attributes
mock 同样可方便的模拟返回值和 attributes,例如模拟一个对象的返回值,
>>> value_mock = mock.Mock()
>>> value_mock.return_value = 3
>>> value_mock()
3
模拟一个方法的返回值:
>>> method_value_mock = mock.Mock()
>>> method_value_mock.method.return_value = 3
>>> method_value_mock.method()
3
模拟对象的 attribute:
>>> attr_mock = mock.Mock()
>>> attr_mock.x = 3
>>> attr_mock.x
3
参数 side_effect
side_effect 是一个非常有用的参数,大大提高了 mock 返回值的灵活性,它可以是一个异常、函数或者可迭代对象。例如返回一个异常:
>>> except_mock = mock.Mock(side_effect=Exception('Boom!'))
>>> except_mock()
Traceback (most recent call last):
...
Exception: Boom!
当 side_effect 为迭代对象时,样例如下:
>>> iter_mock = mock.Mock(side_effect=[1, 2, 3])
>>> iter_mock()
1
>>> iter_mock()
2
>>> iter_mock()
3
>>> iter_mock()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Python/2.7/site-packages/mock/mock.py", line 1062, in __call__
return _mock_self._mock_call(*args, **kwargs)
File "/Library/Python/2.7/site-packages/mock/mock.py", line 1121, in _mock_call
result = next(effect)
File "/Library/Python/2.7/site-packages/mock/mock.py", line 127, in next
return _next(obj)
StopIteration
单 side_effect 为函数时,样例如下:
>>> def side_effect(value):
... return value
...
>>>
>>> side_effect_mock = mock.Mock(side_effect=side_effect)
>>> side_effect_mock(1)
1
>>> side_effect_mock("hello world!")
'hello world!'