pytest Mock 与 patch:模拟依赖让单元测试更独立

小飞兽 Python 8 次阅读 2026-07-24

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

---
有问题欢迎在评论区交流!