Python类Mock异常:命名空间包可用,常规包失效
关于Python单元测试中Mock配置类的问题与解答
上下文
我正在给应用写单元测试,configuration/connections.py模块里有个通过环境变量定义的配置类:
from typing import Literal from pydantic_settings import BaseSettings class StorageConfig(BaseSettings): storage_name: Literal["file", "s3"]
这个配置在connections/storage/main.py里被调用:
import fsspec from configuration.connections import StorageConfig storage_config = StorageConfig() fs: fsspec.AbstractFileSystem = fsspec.filesystem(storage_config.storage_name)
因为跑单元测试时没有环境配置,我尝试MockStorageConfig对象:
import pytest from pydantic import BaseModel @pytest.fixture(autouse=True) def local_config(mocker): class LocalStorageConfig(BaseModel): storage_name: str = "file" mocker.patch("configuration.connections.StorageConfig", new=LocalStorageConfig)
问题场景
根据文件夹结构不同,Mock的效果时好时坏:
- 当
connections.storage是命名空间包,测试文件放在connections/storage/tests/test_storage.py,直接patchconfiguration.connections.StorageConfig时Mock正常工作(和文档建议的patch位置不一样)。 - 当
connections.storage是常规包,其他条件和上面一致,Mock失效。 - 把测试文件移到
tests/integration/test_storage.py,其他条件和第二个场景一致,Mock又恢复正常。
疑问解答
1. 为什么常规包和命名空间包会影响Mock效果?
核心差异在于Python的模块缓存机制:
- 命名空间包:没有
__init__.py,导入时Python会把所有同名目录合并成一个命名空间,不会提前缓存connections.storage.main对StorageConfig的引用。当你patchconfiguration.connections.StorageConfig后,connections.storage.main后续的导入会直接使用被patch后的类,Mock生效。 - 常规包:带有
__init__.py,Python会把整个包作为模块缓存。当测试文件和connections.storage同目录时,运行测试会优先加载connections.storage.main,此时StorageConfig已经被导入并缓存到main模块的本地空间里——后续patch原始类的操作,无法覆盖已经缓存的引用,导致Mock失效。
2. 测试文件的位置为什么会影响Mock效果?
这和Python的模块搜索路径(sys.path)以及导入顺序直接相关:
- 当测试文件在
connections/storage/tests/下时,运行测试时当前目录会被加到sys.path最前端。此时导入connections.storage.main时,Python会优先从当前目录的父目录加载connections包,导致main模块提前加载,缓存了原始的StorageConfig引用,后续的patch无法覆盖这个缓存。 - 当测试文件移到
tests/integration/下时,sys.path的优先项是项目根目录,此时测试代码会先执行patch操作,再加载connections.storage.main,main里导入的就是被patch后的StorageConfig,Mock自然生效。
3. 为什么按照文档建议patchconnections.storage.main.StorageConfig时无法生效?
问题出在模块加载时机:connections.storage.main模块里,storage_config = StorageConfig()是在模块加载阶段直接执行的。如果patch操作晚于模块加载,那么storage_config已经是用原始类实例化好的对象了——此时patchconnections.storage.main.StorageConfig这个类,不会改变已经创建完成的storage_config实例,所以看起来Mock失效。
正确的做法是要么确保patch在模块加载前执行,要么直接patchconnections.storage.main.storage_config这个已实例化的对象,而非类本身。
内容的提问来源于stack exchange,提问作者Marco Bresson
相关产品推荐
相关产品推荐

