使用 pytest-mock 进行 Python 高级单元测试与模拟
一、单元测试与模拟的意义
在软件开发中,单元测试用于验证代码逻辑的正确性。但实际项目中,代码常依赖外部服务(如数据库、API、文件系统)。直接测试这些依赖会导致:
- 测试速度变慢
- 测试结果不可控
- 产生副作用(如真实发送邮件)
模拟(Mocking) 技术通过创建虚拟对象替代真实依赖,使测试聚焦于当前单元的逻辑。
二、环境安装
使用 pytest
和 pytest-mock
插件:
1
|
pip install pytest pytest-mock
|
三、基础用法示例
场景:邮件发送服务
假设我们有一个邮件发送类 EmailSender
:
1
2
3
4
5
|
# email_sender.py
class EmailSender:
def send(self, to, subject, body):
# 真实发送邮件的逻辑
return True
|
测试时不应实际发送邮件。使用 pytest-mock
模拟 send
方法:
1
2
3
4
5
6
7
8
9
|
# test_email_sender.py
def test_send_email(mocker):
mock_send = mocker.patch("email_sender.EmailSender.send")
sender = EmailSender()
result = sender.send("user@example.com", "Hello", "Test email")
mock_send.assert_called_once_with("user@example.com", "Hello", "Test email")
assert result is True
|
代码解析:
mocker.patch
替换了 EmailSender.send
方法
- 验证方法是否以正确参数被调用
- 断言返回值为
True
四、高级模拟技巧
1. 模拟返回值
1
2
3
4
|
def test_mock_return_value(mocker):
mocker.patch("module.Class.method", return_value="mocked_response")
instance = Class()
assert instance.method() == "mocked_response"
|
2. 模拟抛出异常
1
2
3
4
5
|
def test_mock_exception(mocker):
mocker.patch("module.Class.method", side_effect=Exception("DB Error"))
instance = Class()
with pytest.raises(Exception, match="DB Error"):
instance.method()
|
3. 模拟连续调用
1
2
3
4
5
6
7
|
def test_multiple_returns(mocker):
mock_method = mocker.patch("module.Class.method")
mock_method.side_effect = [10, 20, 30]
assert Class().method() == 10
assert Class().method() == 20
assert Class().method() == 30
|
4. 模拟属性与方法链
1
2
3
4
5
6
|
def test_chained_methods(mocker):
mock_instance = mocker.MagicMock()
mock_instance.method1().method2().method3.return_value = "final"
assert mock_instance.method1().method2().method3() == "final"
mock_instance.method1.assert_called_once()
|
五、模拟复杂场景
场景:API 客户端测试
1
2
3
4
5
6
7
|
# api_client.py
import requests
class APIClient:
def get_user_data(self, user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
|
测试时模拟 requests.get
:
1
2
3
4
5
6
7
8
9
10
11
|
def test_api_client(mocker):
mock_response = mocker.MagicMock()
mock_response.json.return_value = {"id": 1, "name": "John"}
mock_get = mocker.patch("requests.get", return_value=mock_response)
client = APIClient()
data = client.get_user_data(1)
mock_get.assert_called_once_with("https://api.example.com/users/1")
assert data == {"id": 1, "name": "John"}
|
六、最佳实践与注意事项
-
精确断言:不仅要验证是否调用,还要检查参数
1
|
mock_method.assert_called_with(arg1, arg2=expected_value)
|
-
使用 autospec
:保持模拟对象与原对象接口一致
1
|
mocker.patch("module.Class", autospec=True)
|
-
避免过度模拟:核心业务逻辑尽量使用真实对象
-
清理模拟:使用 mocker
Fixture 自动清理,避免测试污染
七、总结
通过 pytest-mock
我们可以:
- ✅ 隔离测试环境
- ✅ 模拟各种边界条件
- ✅ 验证对象交互行为
- ✅ 提升测试执行速度
掌握模拟技术能显著提高测试覆盖率与代码质量。建议结合 官方文档 深入学习更复杂的使用场景。