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

Python单元测试因__init__.py覆盖无法正确patch类问题

单元测试patch类时被__init__.py星号导入遮蔽的问题

问题场景

编写代码库单元测试时,需要patch一个数据库抽象类,但该类被__init__.py的导入逻辑遮蔽,无法正确打补丁。

项目文件结构

.
├── __init__.py
├── tools
│   ├── __init__.py
│   ├── test
│   │   ├── __init__.py
│   │   └── test_tool1.py
│   ├── tool1.py
│   └── tool2.py
└── utils
    ├── __init__.py
    └── sql_client.py

各文件现有代码

tools/__init__.py:

from .tool1 import *  # SQLClient随tool1的导出被导入到tools命名空间
from .tool2 import *  # SQLClient再次随tool2的导出被导入,覆盖前一次的导入结果

tools/tool1.py:

from utils import SQLClient

class A(object):
    ...
    def run(self, **kwargs):
        # 待测试的目标方法
        sql = SQLClient("some id")

tools/tool2.py:

from utils import SQLClient
...

utils/__init__.py:

from sql_client import *

utils/sql_client.py:

class SQLClient(object):
    # 需要被patch的数据库抽象类

测试统一使用绝对导入,方便后续将所有测试文件移出源码目录,测试文件tools/test/test_tool1.py现有代码如下:

from unittest.mock import MagicMock
from utils import SQLClient
from tools import A

class MockSQLClient(MagicMock):
    def __init__(self, *args, **kwargs):
        super().__init__(spec=SQLClient)
        self._mocks = {"select *": "rows"}
    def make_query(query):
        return self._mocks[query]

def test_run_func(monkeypatch):
    monkeypatch.setattr("tools.SQLClient", MockSQLClient)
    a = A()
    a.run()
    # 剩余测试逻辑

现存问题

tools/__init__.py通过import *从所有子模块批量导入内容,导致tool1中引入的SQLClient被tool2中引入的同名SQLClient覆盖,最终monkeypatch实际patch的是tool2.SQLClient,不符合测试预期。
尝试直接通过monkeypatch.setattr("tools.tool1.SQLClient")的方式patch tool1模块内的SQLClient,但受tools/__init__.py导入逻辑影响,运行时会抛出tools模块下不存在tool1属性的错误,无法生效。

解决方案

核心要记住Python mock的基本原则:永远patch测试代码中实际引用目标名字的位置,而不是定义目标类的源位置。
遇到的两个问题本质都是踩了Python导入规则的坑,按下面两步修改即可解决:

  • 显式导入tools.tool1模块,触发子模块注册
    之前直接传字符串"tools.tool1.SQLClient"报错,是因为tools/__init__.py没有把tool1模块本身导入到tools的命名空间,只是把tool1里的内容拉到了tools命名空间下。只需要在测试文件开头显式import一次tools.tool1,解释器就会把tool1注册为tools模块的属性,后续patch路径就能正常识别,不会再抛属性不存在的错误。
  • 直接patch tools.tool1模块命名空间下的SQLClient
    目标方法A.run是在tool1.py的代码里直接调用SQLClient,这个名字属于tool1模块自己的命名空间,不管外层__init__.py怎么覆盖导入,只要patch的是tool1模块自己持有的SQLClient引用,run方法调用时拿到的就是替换后的Mock类,不会被tools包层级被覆盖的引用影响。

修改后的测试代码核心部分如下:

from unittest.mock import MagicMock
# 显式导入tools.tool1,触发子模块注册
import tools.tool1
from utils import SQLClient
from tools import A

class MockSQLClient(MagicMock):
    def __init__(self, *args, **kwargs):
        super().__init__(spec=SQLClient)
        self._mocks = {"select *": "rows"}
    # 原测试代码此处漏写self参数,已修正
    def make_query(self, query):
        return self._mocks[query]

def test_run_func(monkeypatch):
    # 直接patch tool1模块命名空间下的SQLClient
    monkeypatch.setattr(tools.tool1, "SQLClient", MockSQLClient)
    a = A()
    a.run()
    # 剩余测试逻辑

额外提示:当前__init__.py里无限制使用import *的写法本身就很容易触发同名覆盖问题,后续维护很容易踩类似的坑,条件允许的话尽量改成显式导入需要对外暴露的名字,或者给模块加__all__字段明确星号导入的内容范围,避免无意义的命名冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 20:15:52