应使用 unittest.mock.patch 替换被装饰函数的 __wrapped__ 属性(需 functools.wraps 保留),以验证装饰器是否正确调用原始函数;若未保留 __wrapped__ 则 patch 失败,且不可 patch 装饰后函数本身以免绕过装饰逻辑。

用 unittest.mock.patch 替换被装饰函数的原始实现
装饰器通常会包裹原函数,直接调用被装饰函数时看到的是包装后的逻辑,很难验证它是否正确调用了原始函数。这时候不能只测“结果”,得测“行为”——比如是否传了正确参数、是否在特定条件下调用了原函数、是否跳过了执行等。
最稳妥的方式是用 patch 把被装饰函数的 __wrapped__(如果存在)或原始函数本身替换为 Mock,再触发装饰后函数的调用:
from unittest.mock import patch
import pytest
<p>def my_decorator(func):
def wrapper(*args, *<em>kwargs):
print("before")
result = func(</em>args, **kwargs)
print("after")
return result
return wrapper</p><p>@my_decorator
def target():
return 42</p><h1>测试:确认 wrapper 确实调用了原函数</h1><p>def test_decorator_calls_original():
with patch("<strong>main</strong>.target.<strong>wrapped</strong>", return_value=99) as mock_wrapped:
result = target()
assert result == 99
assert mock_wrapped.called
- 必须确保装饰器保留了
__wrapped__属性(functools.wraps会自动处理);否则得在装饰器里手动设wrapper.__wrapped__ = func - 如果装饰器不带
@functools.wraps(func),__wrapped__不存在,patch会失败,报AttributeError: __wrapped__ - 别 patch
target本身——那会绕过装饰器,失去测试意义
检查装饰后函数的签名和元信息是否被正确保留
很多装饰器忘了用 @functools.wraps(func),导致 help()、IDE 提示、类型检查甚至某些框架(如 FastAPI 路由解析)出错。这不是“功能 bug”,但会让协作和维护变困难。
可以写个简单断言来守住底线:
立即学习“Python免费学习笔记(深入)”;
import inspect from functools import wraps <p>def bad_decorator(func): def wrapper(*args, *<em>kwargs): return func(</em>args, **kwargs) return wrapper # ❌ 没有 wraps</p><p>def good_decorator(func): @wraps(func) def wrapper(*args, *<em>kwargs): return func(</em>args, **kwargs) return wrapper</p><p>def example(a: int, b: str = "default") -> float: """example docstring""" return float(a)</p><p>assert inspect.signature(example) == inspect.signature(bad_decorator(example)) # ❌ 失败 assert inspect.signature(example) == inspect.signature(good_decorator(example)) # ✅ 通过 assert example.<strong>doc</strong> == good_decorator(example).<strong>doc</strong> # ✅
-
inspect.signature()是最敏感的检测项,装饰后签名错一个参数名或默认值,就会不等 -
__name__和__doc__也建议校验,尤其当函数要被文档工具(Sphinx)或 CLI 框架(Click)扫描时 - 如果装饰器本身接受参数(比如
@retry(times=3)),记得测试的是最终返回的 wrapper,不是外层闭包
对带条件逻辑的装饰器做分支覆盖(如缓存、重试、权限)
像 @lru_cache、@retry、@require_role("admin") 这类装饰器,行为高度依赖运行时输入或状态。光测“能跑通”没用,得让每条控制路径都被触发。
关键不是模拟“外部服务”,而是控制装饰器内部的状态变量或判断条件:
from functools import lru_cache <p>@lru_cache(maxsize=1) def expensive_call(x): return x * 2</p><h1>测试缓存命中与未命中</h1><p>def test_lru_cache_behavior(): expensive_call(1) # 第一次,未命中 assert expensive_call.cache_info().misses == 1 expensive_call(1) # 第二次,命中 assert expensive_call.cache_info().hits == 1
- 优先使用装饰器自带的调试接口(如
cache_info()、retry.statistics),比 mock 内部逻辑更稳定 - 对自定义重试装饰器,可把
time.sleep或random.randompatch 掉,强制让它在第一次就成功/失败 - 权限类装饰器,重点测拒绝路径:传无效 token、缺 header、角色不匹配——这些往往比“正常通过”更易出错
避免在测试中过度依赖装饰器的具体实现细节
比如硬编码去 assert mock.call_args_list[0][0][0] == "before",这其实是把测试绑死在当前打印语句上。一旦装饰器改了日志内容,测试就挂,但功能可能完全没变。
真正该关注的是:是否改变了原函数的输入输出契约?是否引入了额外副作用(如修改全局状态、发起网络请求)?是否在不该调用的时候调用了?
- 用
side_effect控制原函数行为,比断言装饰器内部 print 更可靠 - 如果装饰器做了 I/O,把它隔离成可注入的依赖(例如传入一个
http_client参数),测试时直接换 stub - 不要测“装饰器长什么样子”,而要测“它让函数变得怎么样”——这是行为驱动的思维落点
装饰器测试最难的部分,往往不是写断言,而是想清楚:你到底想证明它没破坏什么,又保证了什么。漏掉对 __wrapped__ 的校验、或者把 mock 打在错误的目标上,是实际项目里最常重复踩的坑。


















