如何在单元测试中正确模拟类实例内部调用的函数

本文详解如何在 python 单元测试中精准 mock 被类方法内部调用的外部函数(如 `get_player_account_status`),重点解决路径指定错误、作用域不匹配及 `side_effect` 动态返回等常见问题,并提供可复现的修复方案。

在测试 PlayerAnomalyDetectionModel.fit() 时,你遇到的核心问题并非 side_effect 使用不当,而是 mock 路径错误——这是 unittest.mock.patch 最常见的陷阱。关键原则是:必须 patch 函数被“导入并使用”的位置,而非其“定义位置”

回顾你的代码结构:

  • get_player_account_status 在 get_player_labels.py 中定义;
  • 但在 model.py 中通过 from get_player_labels import get_player_account_status 导入并直接调用;
  • 因此,PlayerAnomalyDetectionModel._set_thresholds() 内部实际引用的是 model.get_player_account_status(即导入后的符号),而非 get_player_labels.get_player_account_status。

✅ 正确 patch 路径应为:'model.get_player_account_status'
❌ 错误路径(导致 mock 失效):'get_player_labels.get_player_account_status' 或 'model.get_player_labels.get_player_account_status'

此外,你的测试类混合了 pytest fixture 和 unittest.TestCase,存在兼容性风险(@mock.patch 装饰器对 unittest.TestCase 子类有效,但 @pytest.fixture(autouse=True) 在 unittest 类中行为不可靠)。建议统一风格,推荐纯 pytest 方案(更简洁、无继承冲突):

✅ 推荐修复后的 test_model.py 片段:

import pandas as pd
import pytest
from unittest.mock import patch
from model import PlayerAnomalyDetectionModel


@pytest.fixture
def sample_train_data():
    # 构造示例数据(此处省略具体实现)
    return pd.DataFrame({"player": ["p1", "p2", "p3"], "score": [1.0, 2.0, 3.0]})


# 使用 pytest 风格:直接 patch model 模块中的函数
def test_fit_with_mocked_account_status(sample_train_data):
    model = PlayerAnomalyDetectionModel()

    # 指定正确的 patch 路径:model 模块内导入的函数名
    with patch('model.get_player_account_status') as mock_get_status:
        # 配置 side_effect 实现每次调用返回不同值
        mock_get_status.side_effect = [
            'open', 'open', 'tosViolation', 'tosViolation', 'tosViolation', 'closed',
            'open', 'open', 'tosViolation', 'tosViolation', 'tosViolation', 'closed'
        ]

        # 执行被测方法
        model.fit(sample_train_data, generate_plots=False)

        # 断言 mock 被调用(可选验证)
        assert mock_get_status.call_count == 12  # 根据实际逻辑调整预期次数

⚠️ 关键注意事项:

  • 路径必须精确:若 model.py 中写的是 from get_player_labels import get_player_account_status,则 patch 'model.get_player_account_status';若写的是 import get_player_labels 且调用 get_player_labels.get_player_account_status(...),才需 patch 'model.get_player_labels.get_player_account_status'。
  • 避免混合测试框架:unittest.TestCase 与 @pytest.fixture 共存易引发 setup/teardown 行为异常。纯 pytest 更可靠。
  • side_effect 用法正确:你原写法本身无误——列表形式 side_effect = [...] 会按顺序逐次返回,完全满足“每次调用不同值”的需求。
  • Workaround 的局限性:手动预设 _account_statuses 虽能绕过 API 调用,但会使测试脱离真实执行路径(例如跳过了 _set_thresholds 中的 while 循环逻辑),降低测试保真度。应优先采用正确 mock。

✅ 总结

正确 mock 的三要素:

  1. 定位准:patch 函数在被测代码所在模块中的导入名
  2. 作用域对:patch 必须在被测方法执行前生效(with patch 或装饰器);
  3. 验证全:通过 call_count、assert_called_with() 等校验 mock 行为是否符合预期。

遵循以上原则,即可稳定拦截类内部的外部函数调用,彻底避免真实 API 请求,构建健壮、快速、可重复的单元测试。