当前位置:首页>python>Python 测试中的 Mock 与 Stub:unittest.mock 完全指南

Python 测试中的 Mock 与 Stub:unittest.mock 完全指南

  • 2026-09-12 09:54:39
Python 测试中的 Mock 与 Stub:unittest.mock 完全指南

在编写 Python 单元测试时,你是否经常遇到这些问题?

❌ 调用支付网关,但不想真扣钱

❌ 依赖数据库,但 CI 环境没装 MySQL

❌ 调用天气 API,但网络不稳定导致测试失败

❌ 时间函数 datetime.now() 让断言无法确定

根本原因:你的测试没有做到 完全隔离(Isolation)。

而解决之道,就是 Mock 与 Stub —— Python 标准库 unittest.mock 提供的强大工具。

今天,我们就从 概念辨析 → 核心 API → 实战场景 → 高级技巧,彻底掌握它。‍

🔍 一、Mock vs Stub:别再混淆!

概念
目的
行为
典型用途
Stub(桩)
提供预设响应
返回固定值(如 {"status": "success"})
替代外部服务,避免真实调用
Mock(模拟)
验证交互行为
记录被调用次数、参数,并可断言
确保代码按预期调用了依赖

💡 简单记:

Stub = “假数据提供者”Mock = “行为记录仪 + 断言器”

在 unittest.mock 中,Mock 对象同时具备两者能力——既能返回预设值(Stub),又能验证调用(Mock)。‍

🧱 二、核心对象:Mock、MagicMock、patch

1. Mock:万能替身

from unittest.mock import Mock# 创建一个 Mock 对象fake_db = Mock()# 预设方法返回值(Stub 行为)fake_db.get_user.return_value = {"id": 1, "name": "Alice"}# 调用它user = fake_db.get_user(1)assert user["name"] == "Alice"# 验证是否被调用(Mock 行为)

fake_db.get_user.assert_called_once_with(1)

2. MagicMock:支持魔法方法的 Mock

from unittest.mock import MagicMock# 模拟一个可迭代对象fake_list = MagicMock()fake_list.__iter__.return_value = iter([1, 2, 3])# 可用于 for 循环for item in fake_list:    print(item)  # 输出 1, 2, 3# 也可模拟 len()fake_list.__len__.return_value = 3assert len(fake_list) == 3

3. patch:动态替换(最常用!)

from unittest.mock import patchimport my_module  # 假设它调用了 requests.get# 场景:my_module.fetch_data() 内部调用了 requests.get@patch('my_module.requests.get')  # 注意:路径是被测模块中的导入路径!def test_fetch_data(mock_get):    # 预设返回值    mock_get.return_value.json.return_value = {"data": "test"}    result = my_module.fetch_data()    assert result == "test"    # 验证是否被调用    mock_get.assert_called_once_with("https://api.example.com/data")

⚠️ 关键点:

patch('my_module.requests.get') 中的路径必须是 被测代码中实际使用的路径,而非原始定义路径。‍

🛠️ 三、5 大实战场景(附完整代码)

场景 1:Mock 第三方 HTTP 请求(避免真实网络调用)

# payment.pyimport requestsdef process_payment(amount):    resp = requests.post("https://payment-gateway.com/pay", json={"amount": amount})    return resp.json()["transaction_id"]# test_payment.pyfrom unittest.mock import patch@patch('payment.requests.post')def test_process_payment(mock_post):    # 预设网关返回    mock_post.return_value.json.return_value = {"transaction_id": "txn_123"}    txn_id = process_payment(100)    assert txn_id == "txn_123"    # 验证请求参数    mock_post.assert_called_once_with(        "https://payment-gateway.com/pay",        json={"amount": 100}    )

场景 2:Stub 时间函数(让时间可控)

# report.pyfrom datetime import datetimedef generate_daily_report():    today = datetime.now().strftime("%Y-%m-%d")    return f"Report for {today}"# test_report.pyfrom unittest.mock import patchfrom datetime import datetime@patch('report.datetime')def test_generate_daily_report(mock_datetime):    # 固定当前时间为 2025-01-01    mock_datetime.now.return_value = datetime(2025, 1, 1)    mock_datetime.strftime = lambda self, fmt: self.strftime(fmt)    report = generate_daily_report()    assert report == "Report for 2025-01-01"💡 技巧:Mock datetime 时需同时处理 now() 和 strftime。

场景 3:Mock 数据库连接(无需启动 DB)

# user_service.pyimport sqlite3def get_user(user_id):    conn = sqlite3.connect("app.db")    cursor = conn.cursor()    cursor.execute("SELECT name FROM users WHERE id=?", (user_id,))    row = cursor.fetchone()    conn.close()    return row[0] if row else None# test_user_service.pyfrom unittest.mock import patch, MagicMock@patch('user_service.sqlite3')def test_get_user(mock_sqlite3):    # 模拟数据库连接和游标    mock_conn = MagicMock()    mock_cursor = MagicMock()    mock_sqlite3.connect.return_value = mock_conn    mock_conn.cursor.return_value = mock_cursor    mock_cursor.fetchone.return_value = ("Alice",)    name = get_user(1)    assert name == "Alice"    # 验证 SQL 是否正确执行    mock_cursor.execute.assert_called_once_with(        "SELECT name FROM users WHERE id=?", (1,)    )

场景 4:验证异常处理逻辑

# api_client.pyimport requestsdef fetch_user(user_id):    try:        resp = requests.get(f"https://api.example.com/users/{user_id}")        resp.raise_for_status()        return resp.json()    except requests.RequestException:        return {"error": "Network failed"}# test_api_client.pyfrom unittest.mock import patchimport requests@patch('api_client.requests.get')def test_fetch_user_network_error(mock_get):    # 模拟网络异常    mock_get.side_effect = requests.RequestException("Timeout")    result = fetch_user(123)    assert result == {"error": "Network failed"}

✅ side_effect 可抛出异常、返回不同值序列,或执行自定义函数。

场景 5:Mock 类实例(替代 __init__)

# email_service.pyclass EmailClient:    def __init__(self, api_key):        self.api_key = api_key    def send(self, to, subject):        # 调用真实邮件服务        passdef notify_user(user_email):    client = EmailClient("SECRET_KEY")    client.send(user_email, "Welcome!")# test_email_service.pyfrom unittest.mock import patch@patch('email_service.EmailClient')def test_notify_user(mock_client_class):    # mock_client_class 是类的 Mock    mock_instance = mock_client_class.return_value  # 实例的 Mock    notify_user("alice@example.com")    # 验证是否创建了实例    mock_client_class.assert_called_once_with("SECRET_KEY")    # 验证实例方法是否被调用    mock_instance.send.assert_called_once_with("alice@example.com", "Welcome!")‍

🚀 四、高级技巧:写出更健壮的 Mock 测试

技巧 1:使用 spec 限制 Mock 行为(防拼写错误)

from unittest.mock import Mock# 假设真实类有 connect() 和 close() 方法class RealDB:    def connect(self): pass    def close(self): pass# 创建带 spec 的 Mockfake_db = Mock(spec=RealDB)fake_db.connect()      # OKfake_db.connet()       # 拼写错误!会抛出 AttributeError

技巧 2:patch 作为上下文管理器(局部 Mock)

def test_with_context():    with patch('my_module.external_api') as mock_api:        mock_api.return_value = "mocked"        result = my_module.call_api()        assert result == "mocked"    # 退出 with 后自动恢复原对象

技巧 3:Mock 链式调用(常见于 ORM)

# 模拟 User.objects.filter(active=True).first()mock_user = Mock()mock_queryset = Mock()mock_manager = Mock()mock_manager.filter.return_value = mock_querysetmock_queryset.first.return_value = mock_userwith patch('my_app.models.User.objects', mock_manager):    user = User.objects.filter(active=True).first()    assert user == mock_user‍

⚠️ 五、避坑指南:常见错误与最佳实践

陷阱
正确做法
Mock 错误路径
patch('被测模块.导入的名称')
,不是原始定义路径
过度 Mock
只 Mock 外部依赖,不 Mock 自己的业务逻辑
忽略 side_effect
用 side_effect 模拟异常或动态行为
未验证调用
不仅要检查返回值,还要用 assert_called* 验证交互
全局状态污染
使用 patch 而非直接替换模块属性

✅ 总结:Mock 的核心心法

Mock 不是为了“绕过”依赖,而是为了“聚焦”被测逻辑。

Stub:回答“如果外部返回 X,我的代码是否正确?”

Mock:回答“我的代码是否按预期调用了外部?”

当你能熟练运用 unittest.mock,你就能:

✅ 编写 快速、稳定、可并行 的单元测试

✅ 在 无网络、无数据库 的 CI 环境中运行测试

✅ 100% 覆盖 异常分支(如网络超时、API 错误)‍

📣 行动建议

打开你的一个测试文件,找出真实调用外部服务的地方

用 @patch 将其 Mock 掉

添加 assert_called* 验证调用行为

真正的单元测试,应该像真空环境中的实验——只观察被测对象本身。

 每一次互动,皆是鼓励, 每一份支持,共促成长。

商务合作:RYXtest

最新文章

随机文章