如何对Flask-Caching缓存的memoize函数进行单元测试?
环境版本
- Python 3.9.13
- pytest==7.2.0
- pytest-mock==3.10.0
- Flask==2.2.2
- Flask-Caching==2.0.1
待测试类代码
from dataclasses import dataclass from flask_caching import Cache # 假设全局已初始化缓存实例 cache = Cache() @dataclass(frozen=True) class NominationData: title: str url: str is_approved: bool articles: list[ArticleData] hooks: list[HookData] @staticmethod @cache.memoize(timeout=30) def from_nomination(nomination): """Construct a NominationData from a dyk_tools.Nomination""" return NominationData( nomination.title(), nomination.url(), nomination.is_approved(), [ArticleData.from_article(a) for a in nomination.articles()], [HookData.from_hook(h) for h in nomination.hooks()], )
问题描述
未添加@cache.memoize()装饰器时,NominationData.from_nomination()的单元测试可正常运行;添加装饰器后,测试在flask_caching/__init__.py:870处报错AttributeError: 'Cache' object has no attribute 'app',原因是缺少有效的Flask应用上下文。尝试过mock Flask上下文、拆分缓存逻辑为独立垫片函数两种思路,但均不够理想,求最优解决方法。
最优解决方案
推荐两种方向,可根据测试需求选择:
方向一:在测试中初始化最小Flask上下文(适合验证缓存行为的场景)
直接创建最小Flask应用实例并绑定缓存,为测试提供合法上下文,无需mock核心逻辑:
- 编写pytest fixture初始化应用和上下文:
import pytest from flask import Flask from your_module import cache, NominationData @pytest.fixture(scope="module") def test_app(): app = Flask(__name__) # 使用内存缓存,避免依赖外部存储,适配单元测试场景 app.config["CACHE_TYPE"] = "SimpleCache" # 将全局缓存绑定到测试应用 cache.init_app(app) return app @pytest.fixture(scope="function") def app_context(test_app): # 为每个测试函数注入应用上下文 with test_app.app_context(): yield
- 在测试函数中使用
app_contextfixture:
def test_from_nomination(app_context, mocker): # Mock 依赖的nomination对象及方法 mock_nom = mocker.Mock() mock_nom.title.return_value = "测试标题" mock_nom.url.return_value = "https://example.com" mock_nom.is_approved.return_value = True # Mock articles和hooks的返回结果 mock_article = mocker.Mock() mock_nom.articles.return_value = [mock_article] mock_hook = mocker.Mock() mock_nom.hooks.return_value = [mock_hook] # Mock 嵌套的from_article和from_hook方法 mocker.patch("your_module.ArticleData.from_article", return_value=mocker.Mock()) mocker.patch("your_module.HookData.from_hook", return_value=mocker.Mock()) # 执行测试并断言 result = NominationData.from_nomination(mock_nom) assert result.title == "测试标题" assert result.url == "https://example.com" assert result.is_approved is True
该方法可完整测试带缓存的逻辑,包括缓存命中、超时等行为,适合需要验证缓存有效性的集成测试场景。
方向二:拆分业务逻辑与缓存逻辑(适合专注测试业务逻辑的场景)
将缓存装饰器与核心业务逻辑分离,让单元测试直接针对无缓存的业务逻辑,避免上下文依赖:
- 重构原代码:
@dataclass(frozen=True) class NominationData: # 字段保持不变 @staticmethod def _from_nomination(nomination): """核心业务逻辑,无缓存依赖""" return NominationData( nomination.title(), nomination.url(), nomination.is_approved(), [ArticleData.from_article(a) for a in nomination.articles()], [HookData.from_hook(h) for h in nomination.hooks()], ) @staticmethod @cache.memoize(timeout=30) def from_nomination(nomination): """对外暴露的带缓存接口""" return NominationData._from_nomination(nomination)
- 单元测试直接测试核心逻辑:
def test_from_nomination_core(mocker): # Mock 依赖对象 mock_nom = mocker.Mock() mock_nom.title.return_value = "测试标题" mock_nom.url.return_value = "https://example.com" mock_nom.is_approved.return_value = True mock_article = mocker.Mock() mock_nom.articles.return_value = [mock_article] mock_hook = mocker.Mock() mock_nom.hooks.return_value = [mock_hook] mocker.patch("your_module.ArticleData.from_article", return_value=mocker.Mock()) mocker.patch("your_module.HookData.from_hook", return_value=mocker.Mock()) # 直接测试核心业务逻辑 result = NominationData._from_nomination(mock_nom) assert result.title == "测试标题" # 其他业务逻辑断言...
这种方法让单元测试聚焦业务逻辑,无需处理Flask上下文;缓存的有效性可单独通过集成测试验证(比如连续调用两次from_nomination,检查_from_nomination是否仅执行一次)。
内容的提问来源于stack exchange,提问作者Roy Smith
相关产品推荐
相关产品推荐

