本文介绍如何在纯 pytest 环境中,通过 monkeypatch 替换依赖上下文管理器(__enter__/__exit__)的类,使其 database_query 方法返回预设测试数据,从而实现对 Flask 路由的隔离式单元测试。
本文介绍如何在纯 pytest 环境中,通过 `monkeypatch` 替换依赖上下文管理器(`__enter__`/`__exit__`)的类,使其 `database_query` 方法返回预设测试数据,从而实现对 flask 路由的隔离式单元测试。
在测试涉及上下文管理器(如 with Database() as db:)的代码时,不能仅 mock 单个方法(例如 database_query),因为 Database 实例是在 with 语句块内动态创建并进入上下文的。直接 patch 方法无法生效——db 对象来自真实类实例,其方法调用仍会执行原始逻辑。
正确做法是完全替换 Database 类本身,用一个轻量级的模拟类替代,该类实现必需的上下文管理协议(__enter__ 返回自身,__exit__ 无副作用),并在其中硬编码所需返回值。
以下是完整、可直接复用的测试方案:
✅ 正确做法:用 monkeypatch 替换整个类
import pytest
from app import app
from tests.sample_data.api_responses.get_some_stuff import get_some_stuff_api_response
from tests.sample_data.query_responses.get_some_stuff import get_some_stuff_query_response
# 导入实际被使用的 Database 类路径(注意:必须与应用中 import 路径一致)
import app.utils.Database # ← 关键:patch 的目标是此模块下的 Database 类
class MockDatabase:
def __init__(self, *args, **kwargs):
# 模拟 __init__ 行为(可选:支持传参兼容性)
pass
def __enter__(self):
return self # 必须返回 self,以支持 `as db`
def __exit__(self, exc_type, exc_value, traceback):
# 无需关闭连接,空实现即可
pass
def database_query(self, table: str, query: str):
# 直接返回预设的测试响应数据
return get_some_stuff_query_response
@pytest.fixture
def client():
with app.test_client() as client:
yield client
def test_get_some_stuff(client, monkeypatch):
# ✅ 关键:patch 模块中被导入的 Database 类(不是原始定义位置!)
monkeypatch.setattr(app.utils.Database, "Database", MockDatabase)
response = client.get("/api/this/route")
assert response.status_code == 200
assert response.get_json() == get_some_stuff_api_response
⚠️ 注意事项与最佳实践
- Patch 路径必须精准:monkeypatch.setattr(module, "ClassName", MockClass) 中的 module 是应用代码中实际导入并使用的模块路径(即 app.utils.Database),而非 database.py 的原始定义路径。否则 patch 失效。
- __enter__ 必须返回 self:这是上下文管理器协议的核心要求,否则 with Database() as db: 会因 db 为 None 或其他对象而报错。
- 避免 patch 方法级对象:不要尝试 patch("app.utils.Database.Database.database_query") —— 因为 database_query 是在 __enter__ 返回的实例上调用的,而该实例来自真实类,patch 不会生效。
- 保持测试纯净性:Mock 类应仅实现测试所需最小接口,不引入额外依赖或状态,确保测试快速、稳定、可重复。
? 扩展建议(按需)
若需验证不同查询参数对应不同响应,可在 MockDatabase.database_query 中添加简单路由逻辑:
def database_query(self, table: str, query: str):
if "users" in table and "active" in query:
return {"hits": [{"id": 1, "name": "Alice"}]}
elif "orders" in table:
return {"total": 42}
return {"default": True}
通过这种类级别替换策略,你能在零外部依赖(不启动数据库、不调用真实客户端)的前提下,高效、可靠地完成端到端 API 路由测试。











