You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Pytest Mock非注入的导入依赖?

问题描述

我使用pytest进行单元测试,被测类依赖直接导入的模块(非注入式依赖)。被测类导入了config.py模块,其关键代码如下:

import logging
import google.cloud.logging as gcp_logging
from utils.logging import logger
import utils.auth as auth

# ... 其他代码

self.project_id = auth.get_project_id()
logger = logging.getLogger('NAME')
client = gcp_logging.Client()

我尝试用pytest fixture结合mocker Mock这些依赖,但Mock未生效,测试仍调用真实库。测试代码与Fixture代码如下:

测试代码

import pytest

def test_create_asset_ids_for_yt_video_ids_for_empty_input(mocked_gads_mgr):
    assert mocked_gads_mgr.create_asset_ids_for_yt_video_ids([]) == {}

Fixture代码

@pytest.fixture
def mocked_gads_mgr(mocker: pytest_mock.MockerFixture, mocked_config):
    # ... 其他代码
    mocker.patch('logging.basicConfig', return_value=None)
    mock_client = mocker.patch('google.cloud.logging.Client')
    mock_client.return_value = MagicMock()
    return gads.GoogleAdsManager.from_config(mocked_config)

运行测试时触发错误:

../../config.py:22: in <module>
    from utils.logging import logger
../../utils/logging.py:32: in <module>
    client = gcp_logging.Client()

raise exceptions.DefaultCredentialsError(_CLOUD_SDK_MISSING_CREDENTIALS)
E google.auth.exceptions.DefaultCredentialsError: 找不到默认凭据,请设置应用默认凭据。

请问如何正确Mock这些非注入的导入依赖?


解决方案

核心问题是Mock路径错误,Python的Mock补丁必须打在依赖被实际引用的模块命名空间中,而非依赖的原始模块路径。以下是具体修正步骤:

1. 修正google.cloud.logging.Client的Mock路径

错误日志显示client = gcp_logging.Client()在utils.logging模块中执行,因此需补丁utils.logging.gcp_logging.Client而非原始路径:

# 替换原有的mock_client = mocker.patch('google.cloud.logging.Client')
mock_client = mocker.patch('utils.logging.gcp_logging.Client')
mock_client.return_value = MagicMock()

2. 修正auth.get_project_id()的Mock路径

若auth.get_project_id()在config.py中被调用,需补丁config.auth.get_project_id而非utils.auth.get_project_id:

# 在fixture中添加该行
mocker.patch('config.auth.get_project_id', return_value='test-project-id')

3. 确保Mock在模块导入前执行

如果config.py或utils.logging在测试模块导入时就执行了初始化代码(比如模块级别的client = gcp_logging.Client()),需将Mock放在模块导入前。可使用autouse=True的fixture实现:

@pytest.fixture(autouse=True)
def mock_external_deps(mocker):
    # 先完成所有依赖Mock,再让被测模块导入
    mocker.patch('utils.logging.gcp_logging.Client', return_value=MagicMock())
    mocker.patch('config.auth.get_project_id', return_value='test-project-id')
    # Mock模块级别的logger初始化
    mocker.patch('logging.getLogger', return_value=MagicMock())

4. 完整修正后的Fixture示例

import pytest
from unittest.mock import MagicMock
import gads

@pytest.fixture
def mocked_gads_mgr(mocker: pytest_mock.MockerFixture, mocked_config):
    # 按依赖实际使用的模块路径打补丁
    mocker.patch('logging.basicConfig', return_value=None)
    mocker.patch('utils.logging.gcp_logging.Client', return_value=MagicMock())
    mocker.patch('config.auth.get_project_id', return_value='test-project-id')
    mocker.patch('logging.getLogger', return_value=MagicMock())
    
    return gads.GoogleAdsManager.from_config(mocked_config)

关键原理

Python导入机制中,当模块A导入模块B的对象时,该对象会绑定到模块A的命名空间。因此Mock必须针对模块A中的引用,而非模块B的原始对象,遵循「哪里用,哪里打补丁」的原则。


内容的提问来源于stack exchange,提问作者Elad Benda

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 20:13:15