pytest-mock mock的高层封装
pytest-mock
pytest-mock是一个pytest的插件,安装即可使用。 它提供了一个名为mocker
的fixture,仅在当前测试function或method生效,而不用自行包装。
object
mock一个object,是最常见的需求。 由于function也是一个object,以下以function举例。
import os
def rm(filename):
os.remove(filename)
def test_rm(mocker):
filename = 'test.file'
mocker.patch('os.remove')
rm(filename)
os.remove.assert_called_once_with(filename)
这里在给os.remove
打了一个patch,让它变成了一个MagicMock。 然后利用assert_called_once_with
,查看它是否被调用一次,并且参数为filename
。
注意:只能对已经存在的东西使用mock。
method
有时,仅仅需要mock一个object里的method,而无需mock整个object。 例如,在对当前object的某个method进行测试时。 这时,可以用patch.object
。
class ForTest:
field = 'origin'
def method():
pass
def test_for_test(mocker):
test = ForTest()
mock_method = mocker.patch.object(test, 'method')
test.method()
assert mock_method.called
assert 'origin' == test.field
mocker.patch.object(test, 'field', 'mocked')
assert 'mocked' == test.field
上例中,分别对field和method进行了mock。 当然,对一个给定module的function,也能使用。
def test_patch_object_listdir(mocker):
mock_listdir = mocker.patch.object(os, 'listdir')
os.listdir()
assert mock_listdir.called
用spy包装
如果只是想用MagicMock包装一个东西,而又不想改变其功能,可以用spy
。
def test_spy_listdir(mocker):
mock_listdir = mocker.spy(os, 'listdir')
os.listdir()
assert mock_listdir.called
与上例中的patch.object
不同的是,上例的os.listdir()
不会真的执行,而本例中则会真的执行。
pytest-mock
This plugin installs a mocker
fixture which is a thin-wrapper around the patching API provided by the mock package, but with the benefit of not having to worry about undoing patches at the end of a test:
import os
class UnixFS:
@staticmethod
def rm(filename):
os.remove(filename)
def test_unix_fs(mocker):
mocker.patch('os.remove')
UnixFS.rm('file')
os.remove.assert_called_once_with('file')
Professionally supported pytest-mock is now available
Usage
The mocker
fixture has the same API as mock.patch, supporting the same arguments:
def test_foo(mocker):
# all valid calls
mocker.patch('os.remove')
mocker.patch.object(os, 'listdir', autospec=True)
mocked_isfile = mocker.patch('os.path.isfile')
The supported methods are:
- mocker.patch
- mocker.patch.object
- mocker.patch.multiple
- mocker.patch.dict
- mocker.stopall
mocker.resetall()
: calls reset_mock() in all mocked objects up to this point.
These objects from the mock
module are accessible directly from mocker
for convenience:
- Mock
- MagicMock
- PropertyMock
- ANY
- DEFAULT (Version 1.4)
- call (Version 1.1)
- sentinel (Version 1.2)
- mock_open
Spy
The spy acts exactly like the original method in all cases, except it allows use of mock
features with it, like retrieving call count. It also works for class and static methods.
def test_spy(mocker):
class Foo(object):
def bar(self):
return 42
foo = Foo()
mocker.spy(foo, 'bar')
assert foo.bar() == 42
assert foo.bar.call_count == 1
Since version 1.11
, it is also possible to query the return_value
attribute to observe what the spied function/method returned.
Since version 1.13
, it is also possible to query the side_effect
attribute to observe any exception thrown by the spied function/method.
Stub
The stub is a mock object that accepts any arguments and is useful to test callbacks, for instance. May be passed a name to be used by the constructed stub object in its repr (useful for debugging).
def test_stub(mocker):
def foo(on_something):
on_something('foo', 'bar')
stub = mocker.stub(name='on_something_stub')
foo(stub)
stub.assert_called_once_with('foo', 'bar')
Improved reporting of mock call assertion errors
This plugin monkeypatches the mock library to improve pytest output for failures of mock call assertions like Mock.assert_called_with()
by hiding internal traceback entries from the mock
module.
It also adds introspection information on differing call arguments when calling the helper methods. This features catches AssertionError raised in the method, and uses py.test's own advanced assertions to return a better diff:
mocker = <pytest_mock.MockFixture object at 0x0381E2D0> def test(mocker): m = mocker.Mock() m('fo') > m.assert_called_once_with('', bar=4) E AssertionError: Expected call: mock('', bar=4) E Actual call: mock('fo') E E pytest introspection follows: E E Args: E assert ('fo',) == ('',) E At index 0 diff: 'fo' != '' E Use -v to get the full diff E Kwargs: E assert {} == {'bar': 4} E Right contains more items: E {'bar': 4} E Use -v to get the full diff test_foo.py:6: AssertionError ========================== 1 failed in 0.03 seconds ===========================
This is useful when asserting mock calls with many/nested arguments and trying to quickly see the difference.
This feature is probably safe, but if you encounter any problems it can be disabled in your pytest.ini
file:
[pytest]
mock_traceback_monkeypatch = false
Note that this feature is automatically disabled with the --tb=native
option. The underlying mechanism used to suppress traceback entries from mock
module does not work with that option anyway plus it generates confusing messages on Python 3.5 due to exception chaining
Use standalone "mock" package
New in version 1.4.0.
Python 3 users might want to use a newest version of the mock
package as published on PyPI than the one that comes with the Python distribution.
[pytest]
mock_use_standalone_module = true
This will force the plugin to import mock
instead of the unittest.mock
module bundled with Python 3.4+. Note that this option is only used in Python 3+, as Python 2 users only have the option to use the mock
package from PyPI anyway.
Note about usage as context manager
Although mocker's API is intentionally the same as mock.patch
's, its use as context manager and function decorator is not supported through the fixture:
def test_context_manager(mocker):
a = A()
with mocker.patch.object(a, 'doIt', return_value=True, autospec=True): # DO NOT DO THIS
assert a.doIt() == True
The purpose of this plugin is to make the use of context managers and function decorators for mocking unnecessary.
Requirements
- Python 2.7, Python 3.4+
- pytest
- mock (for Python 2)
Install
Install using pip:
$ pip install pytest-mock
Changelog
Please consult the changelog page.
Why bother with a plugin?
There are a number of different patch
usages in the standard mock
API, but IMHO they don't scale very well when you have more than one or two patches to apply.
It may lead to an excessive nesting of with
statements, breaking the flow of the test:
import mock
def test_unix_fs():
with mock.patch('os.remove'):
UnixFS.rm('file')
os.remove.assert_called_once_with('file')
with mock.patch('os.listdir'):
assert UnixFS.ls('dir') == expected
# ...
with mock.patch('shutil.copy'):
UnixFS.cp('src', 'dst')
# ...
One can use patch
as a decorator to improve the flow of the test:
@mock.patch('os.remove')
@mock.patch('os.listdir')
@mock.patch('shutil.copy')
def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove):
UnixFS.rm('file')
os.remove.assert_called_once_with('file')
assert UnixFS.ls('dir') == expected
# ...
UnixFS.cp('src', 'dst')
# ...
But this poses a few disadvantages:
- test functions must receive the mock objects as parameter, even if you don't plan to access them directly; also, order depends on the order of the decorated
patch
functions; - receiving the mocks as parameters doesn't mix nicely with pytest's approach of naming fixtures as parameters, or
pytest.mark.parametrize
; - you can't easily undo the mocking during the test execution;
An alternative is to use contextlib.ExitStack
to stack the context managers in a single level of indentation to improve the flow of the test:
import contextlib
import mock
def test_unix_fs():
with contextlib.ExitStack() as stack:
stack.enter_context(mock.patch('os.remove'))
UnixFS.rm('file')
os.remove.assert_called_once_with('file')
stack.enter_context(mock.patch('os.listdir'))
assert UnixFS.ls('dir') == expected
# ...
stack.enter_context(mock.patch('shutil.copy'))
UnixFS.cp('src', 'dst')
# ...
But this is arguably a little more complex than using pytest-mock
.
Contributing
Contributions are welcome! After cloning the repository, create a virtual env and install pytest-mock
in editable mode with dev
extras:
$ pip install --editable .[dev]
$ pre-commit install
Tests are run with tox
, you can run the baseline environments before submitting a PR:
$ tox -e py27,py36,linting
Style checks and formatting are done automatically during commit courtesy of pre-commit.
License
Distributed under the terms of the MIT license.
Security contact information
To report a security vulnerability, please use the Tidelift security contact. Tidelift will coordinate the fix and disclosure.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2017-12-28 ES shrink ——一般是结合rollover一起使用的,一开始没有看懂官方shrink文档,当看了这个之后就明白了
2017-12-28 高效管理 Elasticsearch 中基于时间的索引——本质是在利用滚动模式做数据的冷热分离,热索引可以用ssd
2017-12-28 elasticsearch indices.recovery 流程分析(索引的_open操作也会触发recovery)——主分片recovery主要是从translog里恢复之前未写完的index,副分片recovery主要是从主分片copy segment和translog来进行恢复
2017-12-28 JS垃圾回收——和其他语言一样,JavaScript 的 GC 策略也无法避免一个问题:GC 时,停止响应其他操作,这是为了安全考虑
2016-12-28 怎样打造一个分布式数据库——rocksDB, raft, mvcc,本质上是为了解决跨数据中心的复制