pytest Mock 与 patch:模拟依赖让单元测试更独立
Introduction
写单元测试最头疼的问题:你要测的函数依赖数据库、依赖第三方 API、依赖文件系统。在测试环境里这些东西不一定存在,而且即使存在也不稳定——API 挂了测试就失败,这不对。
用 Mock(模拟对象)可以完美解决这个问题:把真实的外部依赖替换成"假对象",测试函数的逻辑,而不测它的依赖。
---
基础语法
1. unittest.mock 基本用法
from unittest.mock import Mock, patch
# 创建模拟对象
mock_db = Mock()
mock_db.query.return_value = [{"id": 1, "name": "pytest"}]
# 使用模拟对象
result = mock_db.query("SELECT * FROM users")
assert result == [{"id": 1, "name": "pytest"}]
mock_db.query.assert_called_once()
2. patch 作为装饰器
用 @patch 把指定位置的函数替换成 Mock:
from unittest.mock import patch
@patch("requests.get")
def test_fetch_user(mock_get):
mock_get.return_value = Mock(json=lambda: {"name": "pytest", "id": 1})
result = fetch_user(123)
assert result["name"] == "pytest"
mock_get.assert_called_once_with("https://api.example.com/users/123")
注意:patch 的目标必须是"被测代码里引用函数的位置",即 from module import func 时要 patch module.func。
3. pytest.fixture + Mock(推荐方式)
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_email_service():
service = Mock()
service.send.return_value = True
return service
def test_send_welcome_email(mock_email_service):
result = send_welcome_email("user@example.com", mock_email_service)
assert result is True
mock_email_service.send.assert_called_once()
4. Mock 验证调用次数和参数
mock_obj = Mock()
mock_obj.method(1, 2, key="value")
mock_obj.method(3, 4)
# 验证调用次数
assert mock_obj.method.call_count == 2
# 验证最后一次调用
last_call = mock_obj.method.call_args
assert last_call[0] == (3, 4)
assert last_call[1] == {}
# 验证第一次调用
first_call = mock_obj.method.call_args_list[0]
assert first_call[0] == (1, 2)
assert first_call[1]["key"] == "value"
5. side_effect:模拟异常和多种返回值
mock_api = Mock()
# 第一次调用返回 A,第二次抛异常
mock_api.get.side_effect = [
{"status": "ok", "data": [1, 2, 3]},
Exception("网络超时")
]
result = mock_api.get("/first")
assert result["status"] == "ok"
with pytest.raises(Exception, match="网络超时"):
mock_api.get("/second")
---
运行效果
$ pytest test_mock.py -v
test_mock.py::test_fetch_user PASSED [50%]
test_mock.py::test_send_welcome_email PASSED [50%]
test_mock.py::test_api_with_fallback PASSED [100%]
======================== 3 passed ========================
---
常见问题
Q1: patch 路径写错了怎么办?
报错AttributeError: <module> does not have the attribute <func>。检查被测代码里的 import 路径:
# 被测代码这样写的:
from requests import get
# → patch "requests.get"
# 而不是 patch "my_module.get"
Q2: Mock 对象的返回值是 None?
默认所有方法的return_value 不会自动返回——需要显式设置 mock.method.return_value = something。
Q3: 怎么避免全局 patch 污染?
每个测试用完 Mock 后,unittest.mock.reset_mock() 可以重置状态,或者用 patch.object 在 with 语句里局部使用:
def test_with_local_mock():
with patch("module.function") as mock_func:
mock_func.return_value = "faked"
# 测试代码
# patch 自动清理,不影响其他测试
---
延伸阅读
- [[pytest 覆盖率报告]] — 用 pytest-cov 量化测试覆盖
- [[Selenium 浏览器自动化]] — 端到端测试实战
- [[Python 单元测试完全指南]] — unittest vs pytest vs nose2
---
有问题欢迎在评论区交流!