name: pytest-mock-guide description: 使用pytest-mock插件进行模拟测试的指南。当编写需要模拟、补丁、监视或存根的pytest测试时使用。涵盖mocker夹具用法、补丁方法、监视/存根模式以及断言辅助工具。
pytest-mock 使用指南
pytest-mock是一个pytest插件,提供mocker夹具作为Python的unittest.mock补丁API的轻量级包装器。它会在每个测试结束后自动撤销所有模拟。
mocker 夹具
mocker夹具是主要接口。在测试函数中请求它:
def test_example(mocker):
# 所有模拟在此测试后自动清理
mock_func = mocker.patch("module.function")
可用的夹具作用域
| 夹具 | 作用域 | 使用场景 |
|---|---|---|
mocker |
函数 | 默认,每个测试的模拟 |
class_mocker |
类 | 在测试类中共享模拟 |
module_mocker |
模块 | 在测试模块中共享模拟 |
package_mocker |
包 | 在包中共享模拟 |
session_mocker |
会话 | 在整个会话中共享模拟 |
补丁方法
mocker.patch(target, …)
通过点分路径补丁模块级对象:
def test_patch(mocker):
# 补丁os.remove函数
mock_remove = mocker.patch("os.remove")
mock_remove.return_value = None
os.remove("file.txt")
mock_remove.assert_called_once_with("file.txt")
mocker.patch.object(target, attribute, …)
直接补丁对象上的属性:
def test_patch_object(mocker):
import os
mock_remove = mocker.patch.object(os, "remove")
os.remove("file.txt")
mock_remove.assert_called_once_with("file.txt")
mocker.patch.dict(in_dict, values, clear=False)
临时补丁字典:
def test_patch_dict(mocker):
config = {"debug": False}
mocker.patch.dict(config, {"debug": True})
assert config["debug"] is True
# 测试后,config["debug"]恢复为False
mocker.patch.multiple(target, **kwargs)
一次性补丁多个属性:
def test_patch_multiple(mocker):
mocks = mocker.patch.multiple(
"os",
remove=mocker.DEFAULT,
listdir=mocker.DEFAULT
)
os.remove("file.txt")
os.listdir("/tmp")
mocks["remove"].assert_called_once()
mocks["listdir"].assert_called_once()
mocker.patch.context_manager(target, attribute, …)
与patch.object相同,但当模拟用作上下文管理器时不发出警告:
def test_context_manager(mocker):
mock_open = mocker.patch.context_manager(builtins, "open")
# 使用`with mock_open(...)`时无警告
常见补丁参数
| 参数 | 描述 |
|---|---|
new |
替换目标的对象 |
return_value |
调用模拟时返回的值 |
side_effect |
要引发的异常或要调用的函数 |
autospec |
创建匹配目标签名的模拟 |
spec |
用作规范的对象 |
spec_set |
更严格的规范,防止设置新属性 |
create |
允许补丁不存在的属性 |
new_callable |
创建模拟的可调用对象 |
使用 mocker.spy() 进行监视
监视包装真实方法同时跟踪调用:
def test_spy(mocker):
spy = mocker.spy(os.path, "exists")
# 调用真实方法
result = os.path.exists("/tmp")
# 但我们可以检查调用
spy.assert_called_once_with("/tmp")
# 访问返回值
assert spy.spy_return == result
assert spy.spy_return_list == [result] # 所有返回值
监视属性
| 属性 | 描述 |
|---|---|
spy_return |
真实方法的最后一个返回值 |
spy_return_list |
所有返回值的列表 |
spy_return_iter |
迭代器副本(当duplicate_iterators=True时) |
spy_exception |
引发的最后一个异常(如果有) |
监视迭代器
def test_spy_iterator(mocker):
spy = mocker.spy(obj, "get_items", duplicate_iterators=True)
items = list(obj.get_items())
# 访问返回迭代器的副本
spy_items = list(spy.spy_return_iter)
创建存根
mocker.stub(name=None)
创建接受任何参数的存根:
def test_stub(mocker):
callback = mocker.stub(name="my_callback")
some_function(on_complete=callback)
callback.assert_called_once()
mocker.async_stub(name=None)
创建异步存根:
async def test_async_stub(mocker):
callback = mocker.async_stub(name="async_callback")
await some_async_function(on_complete=callback)
callback.assert_awaited_once()
模拟辅助工具
mocker.create_autospec(spec, …)
创建匹配规范签名的模拟:
def test_autospec(mocker):
mock_obj = mocker.create_autospec(MyClass, instance=True)
# 使用错误参数调用会引发TypeError
mock_obj.method() # 如果method()无参数则OK
直接模拟类
通过mocker直接访问模拟类:
def test_mock_classes(mocker):
mock = mocker.Mock()
magic_mock = mocker.MagicMock()
async_mock = mocker.AsyncMock()
property_mock = mocker.PropertyMock()
non_callable = mocker.NonCallableMock()
其他实用工具
def test_utilities(mocker):
# 匹配任何参数
mock.assert_called_with(mocker.ANY)
# 创建用于断言的调用对象
mock.assert_has_calls([mocker.call(1), mocker.call(2)])
# 哨兵对象
result = mocker.sentinel.my_result
# 模拟文件打开
m = mocker.mock_open(read_data="文件内容")
mocker.patch("builtins.open", m)
# 密封模拟以防止新属性
mocker.seal(mock)
管理模拟
mocker.stopall()
立即停止所有补丁:
def test_stopall(mocker):
mocker.patch("os.remove")
mocker.patch("os.listdir")
mocker.stopall() # 两个补丁都停止
mocker.stop(mock)
停止特定补丁:
def test_stop(mocker):
mock_remove = mocker.patch("os.remove")
mocker.stop(mock_remove) # 仅停止此补丁
mocker.resetall()
重置所有模拟而不停止它们:
def test_resetall(mocker):
mock_func = mocker.patch("module.func")
mock_func("arg1")
mocker.resetall()
mock_func.assert_not_called() # 调用历史已清除
带有pytest内省的断言方法
pytest-mock通过pytest的比较增强了断言错误消息:
def test_assertions(mocker):
mock = mocker.patch("module.func")
mock("actual_arg")
# 增强的错误显示预期与实际之间的差异
mock.assert_called_with("expected_arg")
# AssertionError显示:
# Args:
# assert ('actual_arg',) == ('expected_arg',)
可用的断言
调用断言:
assert_called()- 至少调用一次assert_called_once()- 恰好调用一次assert_called_with(*args, **kwargs)- 最后一次调用匹配assert_called_once_with(*args, **kwargs)- 调用一次且参数匹配assert_any_call(*args, **kwargs)- 任何调用匹配assert_has_calls(calls, any_order=False)- 有特定调用assert_not_called()- 从未调用
异步断言(用于AsyncMock):
assert_awaited()assert_awaited_once()assert_awaited_with(*args, **kwargs)assert_awaited_once_with(*args, **kwargs)assert_any_await(*args, **kwargs)assert_has_awaits(calls, any_order=False)assert_not_awaited()
配置选项
在pytest.ini、pyproject.toml或setup.cfg中:
[pytest]
# 启用/禁用增强的断言消息(默认:true)
mock_traceback_monkeypatch = true
# 使用独立的mock包而不是unittest.mock(默认:false)
mock_use_standalone_module = false
常见模式
补丁使用位置(而非定义位置)
# my_module.py
from os.path import exists
def check_file(path):
return exists(path)
# test_my_module.py
def test_check_file(mocker):
# 补丁使用位置,而非定义位置
mocker.patch("my_module.exists", return_value=True)
assert check_file("/any/path") is True
测试异常
def test_exception(mocker):
mock_func = mocker.patch("module.func")
mock_func.side_effect = ValueError("错误消息")
with pytest.raises(ValueError, match="错误消息"):
module.func()
多个返回值
def test_multiple_returns(mocker):
mock_func = mocker.patch("module.func")
mock_func.side_effect = [1, 2, 3]
assert module.func() == 1
assert module.func() == 2
assert module.func() == 3
异步函数模拟
async def test_async(mocker):
mock_fetch = mocker.patch("module.fetch_data")
mock_fetch.return_value = {"data": "value"}
result = await module.fetch_data()
assert result == {"data": "value"}
类方法模拟
def test_class_method(mocker):
mocker.patch.object(MyClass, "class_method", return_value="mocked")
assert MyClass.class_method() == "mocked"
属性模拟
def test_property(mocker):
mock_prop = mocker.patch.object(
MyClass, "my_property",
new_callable=mocker.PropertyMock,
return_value="mocked"
)
obj = MyClass()
assert obj.my_property == "mocked"