根本原因是自定义装饰器未使用@wraps,导致函数元数据(如__name__、__doc__、签名)丢失,pytest无法识别测试函数、匹配fixture;而@pytest.mark.*等内置标记仅添加属性,不替换函数对象。

pytest 本身不提供装饰器,所谓“pytest装饰器”实际是用户自定义装饰器或第三方插件(如 @pytest.mark.parametrize)被误用导致元数据丢失。 根本问题不在 pytest,而在装饰器没加 @wraps。
为什么 @pytest.mark.* 不会丢元数据?
@pytest.mark.parametrize、@pytest.mark.skip 等是 pytest 内置标记,它们不替换函数对象,只是往函数对象上挂一个 __pytest_mark__ 属性(类似 func._pytestmark = [...])。函数本身仍是原函数,__name__、__doc__ 全部保留。你执行 help(test_func) 或 test_func.__name__ 依然看到原始值。
你写的自定义装饰器为什么让 pytest 找不到测试函数?
常见错误是写了个日志/重试/权限类装饰器,但忘了在 wrapper 上加 @wraps(func):
- pytest 发现函数名变成
"wrapper",无法匹配测试命名规则(如test_*) - pytest 的收集器依赖
func.__name__和func.__module__定位测试,这两个属性一旦被覆盖就失效 - 报错现象:
collected 0 items,或Module not found,但函数明明存在
带参数的装饰器(比如 @retry(max_attempts=3))更容易踩坑
这类装饰器有三层函数嵌套,@wraps 必须放在最内层的 wrapper 上,不是外层工厂函数:
- 错:把
@wraps加在decorator函数上 → 没用 - 对:只在真正执行逻辑的那个
wrapper(*args, **kwargs)定义前加@wraps(func) - 多层装饰(如
@log @retry @pytest.mark.asyncio)时,只要其中任意一层漏了@wraps,pytest 就可能读到wrapper而不是原始函数
inspect.signature() 失效会影响 pytest 的 fixture 注入
pytest 用 inspect.signature() 解析函数参数来匹配 fixture 名。如果装饰器没加 @wraps,signature() 返回的是 wrapper(*args, **kwargs),而不是原始函数签名:
- 结果:fixture 名字匹配失败,报
Fixture 'db' not found - 注意:
@wraps不复制__code__,但能确保signature()正确回溯到__wrapped__(前提是装饰器返回的 wrapper 设了该属性) - 所以必须用
@wraps,不能手动赋值__name__或__doc__—— 否则__wrapped__缺失,signature()无法穿透
最容易被忽略的点:@wraps 不是“锦上添花”,而是让装饰器能被 pytest、FastAPI、mypy 等工具识别的最低门槛。漏掉它,函数就不再是它自己了。


















