testtools

   


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!'