
本文讲解为何直接 patch 全局布尔常量(如 use_thing = os.environ.get(...) == "true")在单元测试中失效,并推荐使用依赖注入(dependency injection)重构代码,使配置可注入、逻辑可隔离、测试更可靠。
本文讲解为何直接 patch 全局布尔常量(如 use_thing = os.environ.get(...) == "true")在单元测试中失效,并推荐使用依赖注入(dependency injection)重构代码,使配置可注入、逻辑可隔离、测试更可靠。
在 Python 单元测试中,常见误区是试图通过 @mock.patch.dict(os.environ, ...) 来动态修改环境变量,再期望其影响模块级已求值的常量——但这是无效的。原因在于:USE_THING 在模块导入时(即 import thing 阶段)就已完成计算并固化为 True 或 False,后续对 os.environ 的 patch 不会重新触发该表达式求值。
# ❌ 问题代码(不可测试)
import os
USE_THING = os.environ.get("USE_THING", "False").upper() == "TRUE" # ← 导入时即执行!即使你在测试中使用 @mock.patch.dict(os.environ, {"USE_THING": "True"}),USE_THING 的值早已确定,无法被“刷新”。
✅ 正确解法:将配置从“静态常量”转为“可注入依赖”。核心思想是——不把环境判断逻辑藏在模块顶层,而是显式传递给需要它的组件。
推荐重构方式:基于类的依赖注入
# thing.py
import os
class ThingService:
def __init__(self, use_thing: bool):
self.use_thing = use_thing
def do_thing(self):
if not self.use_thing:
return
try:
self._doing_the_thing()
except Exception as e:
logger.error(e)
def do_another_thing(self):
if not self.use_thing:
return
try:
self._doing_the_other_thing()
except Exception as e:
logger.error(e)
def _doing_the_thing(self):
# 实际业务逻辑(可单独 mock)
pass
def _doing_the_other_thing(self):
# 实际业务逻辑(可单独 mock)
pass生产环境初始化(一次读取,统一注入)
# app.py —— 应用启动入口
from thing import ThingService
use_thing = os.environ.get("USE_THING", "False").upper() == "TRUE"
thing_service = ThingService(use_thing)
# 向其他组件传递实例(例如传给 Flask/Starlette 的 dependency,或作为参数注入)单元测试:完全可控、无需 patch 环境
# test_thing.py
import unittest
from unittest.mock import patch, MagicMock
from thing import ThingService
class TestThingService(unittest.TestCase):
def test_do_thing_when_enabled(self):
service = ThingService(use_thing=True)
with patch.object(service, '_doing_the_thing') as mock_do:
service.do_thing()
mock_do.assert_called_once()
def test_do_thing_when_disabled(self):
service = ThingService(use_thing=False)
with patch.object(service, '_doing_the_thing') as mock_do:
service.do_thing()
mock_do.assert_not_called()
def test_do_another_thing_respects_config(self):
service = ThingService(use_thing=False)
with patch.object(service, '_doing_the_other_thing') as mock_other:
service.do_another_thing()
mock_other.assert_not_called()✅ 优势总结
- 可测试性强:测试时直接构造不同 use_thing 值的实例,无需操作 os.environ 或 patch 模块变量;
- 职责清晰:环境解析(os.environ.get(...))与业务逻辑彻底分离,符合单一职责原则;
- 易于扩展:未来支持 YAML/JSON 配置、配置中心等,只需修改初始化逻辑,业务类零改动;
- 类型友好:__init__(self, use_thing: bool) 显式声明类型,利于 IDE 提示与 mypy 检查;
- 无副作用:避免模块导入时隐式读取环境、引发意外异常或竞态问题。
⚠️ 补充提醒:若因历史原因必须保留函数式接口,也可封装工厂函数(如 create_thing_service()),但绝不建议在函数内部重复调用 os.environ.get() —— 这会掩盖配置一致性问题,且难以 mock。始终让配置成为显式输入,而非隐藏依赖。

















