在 pytest 插件开发中,理解钩子函数(hook functions)的机制与生命周期是编写高质量扩展的关键。pytest 通过 pluggy 库实现了灵活的钩子系统,其中 `@hookspec` 用于定义接口规范,而 `@hookimpl` 则提供具体实现逻辑。

**区分接口规范与实现逻辑**
`@hookspec` 装饰器标记的函数仅作为接口声明存在,它告诉 pytest 系统“这里有一个可以被调用的钩子”,但不会自动执行。通常这些定义位于 `_pytest.hookspec` 等内部模块中,供插件开发者参考或在其 `conftest.py` 中实现。
相反,`@hookimpl` 装饰器标记的函数是具体的业务逻辑实现。当 pytest 运行到对应阶段时,会遍历已注册的插件,依次调用所有实现了该钩子的 `@hookimpl` 函数。若一个钩子标记了 `firstresult=True`(如 `pytest_collection_modifyitems`),则只执行第一个返回非 None 值的实现;若标记为 `historic=True`,则所有实现都会被调用。

**修改测试收集流程:以过滤目录为例**
若要自定义测试发现过程,例如忽略特定目录下的文件,应关注以下钩子:
1. **`pytest_ignore_collect(collection_path, config)`**:在收集开始前被调用。若返回 `True`,则完全忽略该路径;返回 `None` 表示交由其他插件决定;返回 `False` 则强制收集。此钩子适用于父目录的 conftest 文件。
2. **`pytest_collect_file(file_path, parent)`**:针对特定文件路径创建收集器(Collector)。若需自定义非 Python 文件的收集逻辑,可在此返回一个继承自 `pytest.File` 的子类。
3. **`pytest_collect_directory(path, parent)`**:针对目录路径创建收集器。常用于处理特殊目录结构或自定义目录扫描器。

此外,`pytest_collection_modifyitems(session, config, items)` 允许在收集完成后对已发现的测试项列表进行就地修改(如过滤、重排序)。注意:若在此钩子中移除了某些 item,必须显式调用 `config.hook.pytest_deselected(items=deselected_items)` 通知其他插件,否则可能导致状态不一致。

**自定义断言失败报告**
当测试中的断言失败时,pytest 会尝试生成更友好的错误信息。若需定制此行为,可使用以下钩子:
1. **`pytest_assertrepr_compare(config, op, left, right)`**:用于为比较操作(如 `==`, `!=`)提供自定义解释文本。返回一个字符串列表,第一行为摘要,后续行缩进显示细节。若未实现或返回 None,则使用默认比较逻辑。
2. **`pytest_assertion_pass(item, lineno, orig, expl)`**:在断言通过时触发(需配置 `enable_assertion_pass_hook = true`),可用于记录调试信息或生成自定义日志。

**调用时机与参数含义总结**
- `pytest_ignore_collect`:收集前,路径分析阶段;参数为待检查的路径和配置对象。
- `pytest_collection_modifyitems`:收集后,item 列表过滤/重排;参数包含 session、config 及 items 列表。
- `pytest_assertrepr_compare`:断言失败时,比较操作解释;参数包括运算符、左右操作数。

开发者应在 `conftest.py` 中实现这些钩子,并遵循 pytest 的插件加载顺序(initial conftests 优先)。通过合理组合这些钩子,可实现对测试流程的深度定制,同时保持与 pytest 生态的兼容性。

信息来源