使用Pytest测试带@cache装饰器的类时Mock失效问题
问题:带@cache装饰器的类无法被Mock替换方法
我在用Pytest给带缓存的类写单元测试时遇到了问题:尝试用mockfunc替换类A里的slowfunc,但只有去掉@cache装饰器时Mock才生效。
原测试代码
import pytest from functools import cache from unittest.mock import patch @cache class A: def __init__(self): print("calling init") def slowfunc(self, text): print("calling slowfunc:", text) return False def call(self): return self.slowfunc("blah") class B: def call(self): a = A() return a.call() class Test_B: def mockfunc(self, text): print("calling mockfunc:", text) return True @patch.object(A, 'slowfunc', mockfunc) def test_B(self): b = B() result = b.call() assert result == True if __name__ == "__main__": retcode = pytest.main(["-xs", __file__])
两种运行结果
注释掉@cache时(符合预期)
example.py calling init calling mockfunc: blah . === 1 passed in 0.07s ===
启用@cache时(测试失败)
example.py calling init calling slowfunc: blah F === FAILURES === ___ Test_B.test_B ___ self = <example.Test_B object at 0x7f305eaaa1f0> @patch.object(A, 'slowfunc', mockfunc) def test_B(self): b = B() result = b.call() > assert result == True E assert False == True example.py:27: AssertionError === short test summary info === FAILED example.py::Test_B::test_B - assert False == True !!!! stopping after 1 failures !!! === 1 failed in 0.23s ===
启用@cache后,代码依然调用原slowfunc导致测试失败,就算在测试开始时调用A.cache_clear()也无法解决问题,这是为什么?
原因分析与解决办法
核心原因
@cache装饰类时,本质是把类的实例化过程包装成了一个缓存函数。此时你看到的A已经不是原来的类,而是这个缓存包装函数。
当你用@patch.object(A, 'slowfunc')时,修改的是这个包装函数的属性,而不是实际类的方法。第一次创建A()实例时,缓存会把这个实例存起来,后续调用A()直接返回缓存实例——而这个实例的slowfunc是创建时的原方法,根本没被Mock影响。就算调用A.cache_clear(),也只是清空缓存,但你Mock的是包装函数,新创建的实例依然会用原类的方法。
解决办法
推荐两种实用方案:
1. 先清缓存+上下文管理器Mock
调整测试逻辑,先清空缓存,再用with块临时Mock类方法,确保新创建的实例能用上Mock:
class Test_B: def mockfunc(self, text): print("calling mockfunc:", text) return True def test_B(self): # 清空缓存,确保后续创建新实例 A.cache_clear() # 用上下文管理器临时替换A的slowfunc with patch.object(A, 'slowfunc', self.mockfunc): b = B() result = b.call() assert result == True
这样做的好处是:测试结束后Mock自动还原,不会污染其他测试用例,逻辑也清晰。
2. 换实例缓存的实现方式(长期方案)
把类上的@cache换成类方法实现的实例缓存,让原类保持正常结构,Mock就能正常工作:
class A: def __init__(self): print("calling init") def slowfunc(self, text): print("calling slowfunc:", text) return False def call(self): return self.slowfunc("blah") # 用类方法实现单例缓存 class AWithCache: _cached_instance = None @classmethod def get_instance(cls): if cls._cached_instance is None: cls._cached_instance = cls() return cls._cached_instance # 修改B类的调用逻辑 class B: def call(self): a = AWithCache.get_instance() return a.call()
这种方式下,你可以直接MockA.slowfunc,或者MockAWithCache.get_instance返回一个Mock实例,灵活性更高。
内容的提问来源于stack exchange,提问作者Sander
相关产品推荐
相关产品推荐

