如何用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

