0
点赞
收藏
分享

微信扫一扫

【pytest官方文档】解读- 插件开发之hooks 函数(钩子)

上一节讲到如何安装和使用第三方插件,用法很简单。接下来解读下如何自己开发​​pytest​​插件。

但是,由于一个插件包含一个或多个钩子函数开发而来,所以在具体开发插件之前还需要先学习hooks函数

一、什么是 hooks 函数

简单来说,在 pytest 的代码中,预留出了一些函数供我们修改,以便来改变pytest工作方式,这些函数就是​​hooks函数​​,我们可以直接重写函数里的内容。

比如,在 pytest代码路径​​\Lib\site-packages\_pytest\hookspec.py​​​中,可以看到 pytest 定义好的 hook 规范,方便我们在开发插件的时候参考规范来调用对应的​​hooks​​函数。

【pytest官方文档】解读-  插件开发之hooks 函数(钩子)_测试用例

二、hooks 函数的分类

从​​hooks​​函数的职责分类来看,大概如下几类:

  • Bootstrapping hooks:引导类钩子,用来调用已经早就注册好的内部插件第三方插件
  • Collection hooks:集合类钩子,pytest 调用集合钩子来收集文件和目录。
  • Test running (runtest) hooks:测试运行相关的钩子,所有与测试运行相关的钩子都接收一个​​pytest.Item​​对象。
  • Reporting hooks:与Session 会话相关的钩子。
  • Debugging/Interaction hooks:调试/交互钩子,少有的可以用于特殊的报告或与异常交互的钩子函数。

可供调用的钩子函数有很多,功能也是各式各样的,有兴趣的童鞋可以进一步细看官方文档里的介绍。我们就是要通过不同钩子函数具备的功能,来实现我们自定义的需求。

三、编写 hooks 函数开发本地插件

写一个插件示例。

比如我们平时执行case的时候,一通跑完可能会出现不少失败的case,那通常我可能就会翻控制台的输出来找出哪些case失败了。

但是控制台里输出的信息有很多,于是乎我想直接把测试失败的case信息存到一个本地文件里,我直接打开就可以看到所有失败的case。

先写一个case文件里的建议测试用例:

# content of mytest/tests.py
def test_failed():
assert False

def test_passed():
assert True

def test_failed2():
assert False

然后再同级目录下创建一个​​conftest​​文件,之前聊fixture时候就说过,conftest里的内容就是本地插件了。

先直接放上插件代码:

# content of mytest/conftest.py

import pytest

from pathlib import Path
from _pytest.main import Session
from _pytest.nodes import Item
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter

FAILURES_FILE = Path() / "failures.txt"

@pytest.hookimpl()
def pytest_sessionstart(session: Session):
print("Hello 把苹果咬哭")
if FAILURES_FILE.exists():
FAILURES_FILE.unlink()
FAILURES_FILE.touch()


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo):
outcome = yield
result = outcome.get_result()
if result.when == "call" and result.failed:
try:
with open(str(FAILURES_FILE), "a") as f:
f.write(result.nodeid + "\n")
except Exception as e:
print("ERROR", e)
pass

解析

1. 重写钩子函数

首先,关于​​pathlib​​模块就是用来做一些路径操作的库,因为我要在本地路径中进行文件相关操作。

​def pytest_sessionstart()​​​中做的事情就是先看下本地是否存在这个名字叫​​failures.txt​​的文件,有的话就删除,没有就新建。

为啥用​​pytest_sessionstart​​这个hook函数,因为通过查看官方API文档里的介绍,发现这个钩子函数是在创建Session对象之后,且在执行收集和进入运行测试循环之前调用,所以很适合用在这里。

所以直接重写这个hook函数来实现我们定义的功能。

2. hook函数中的 firstresult

示例中使用hook函数​​pytest_runtest_makereport​​​,同样通过查看官方API介绍,它的作用是为测试用例的每个​​setup​​​、​​运行​​​和​​tearDown​​​阶段创建​​TestReport​​​。而插件要做的事情,就是要在
用例执行后获取到状态,若是失败就存放到本地​​​txt​​文件。

当查看​​hook规范​​​时候,发现一个装饰器参数​​firstresult=True​​。

【pytest官方文档】解读-  插件开发之hooks 函数(钩子)_测试用例_02

由于在大多数情况下,调用hook函数可能还会触发调用多个hook,所以最后的结果会是​​包含所调用钩子函数的非none结果​​。

当​​firstresult=True​​时,调用钩子函数时只要有第一个返回非none结果,就会将该结果作为整个钩子调用的结果。在这种情况下,将不会调用其余钩子函数。

3. hook函数中的 hookwrapper

回到插件代码本身,也用到了一个参数​​hookwrapper=True​​。

【pytest官方文档】解读-  插件开发之hooks 函数(钩子)_钩子函数_03

默认情况下,我们之间重写hook函数来彻底改变它要做的事情,就像插件代码里第一个hook函数​​pytest_sessionstart​​一样。

当​​hookwrapper=True​​时,等于是我们实现了一个hook函数的包装器。钩子包装器是一个生成器函数,它只产生一次。

当 pytest 调用钩子时,首先执行钩子包装器,并像常规钩子一样传递相同的参数。

​yield​​​关键字大家都熟悉了,当代码执行到这里的时候会暂停一下,继续执行下一个钩子,并且会把所有的结果或者异常封装成一个​​result​​​对象返回到​​yield​​这里。

钩子包装器本身并不返回结果,只是在实际的钩子实现的外面做一些其他的事情。

我们的插件功能其实也并不是要修改这个钩子本身测试报告的内容,所以就直接通过​​hookwrapper=True​​​将我们的​​pytest_runtest_makereport​​写成一个包装好的钩子。

接下来就是具体功能的代码,判断当用例测试结果是​​fail​​,就写到本地文件中。

运行
运行一下测试用例,看下我们插件的执行情况。

【pytest官方文档】解读-  插件开发之hooks 函数(钩子)_把苹果咬哭的不规律日常_04

查看下​​failures.txt​​内容,结果正确。

【pytest官方文档】解读-  插件开发之hooks 函数(钩子)_把苹果咬哭的不规律日常_05

四、钩子函数排序/调用示例

存在这样的情况,对于同一个钩子规范,可能会存在多个实现。这种情况下可以使用参数​​tryfirst​​​和​​trylast​​来影响钩子的调用顺序。

# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
# 尽可能早的执行
...


# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
# 尽可能晚的执行
...


# Plugin 3
@pytest.hookimpl(hookwrapper=True)
def pytest_collection_modifyitems(items):
# 会在上面的 tryfirst 之前执行
outcome = yield
# 在执行所有非钩子包装器之后执行

具体执行顺序如下:

  1. ​Plugin3​​​的​​pytest_collection_modifyitems​​​一直调用到​​yield​​,因为它是一个钩子包装器。
  2. ​Plugin1​​​的​​pytest_collection_modifyitems​​​被调用,因为它被标记为​​tryfirst=True​​。
  3. ​Plugin2​​​的​​pytest_collection_modifyitems​​​被调用,因为它被标记为​​trylast=True​​(但即使没有这个标记,它也会在Plugin1之后)。
  4. ​Plugin3​​​的​​pytest_collection_modifyitems​​​继续在​​yield​​​执行代码,​​yield​​​接收一个​​Result​​实例。

关于hook本篇先到此,剩下的内容另起篇幅了。

最后,闻道有先后,文章有遗漏,欢迎交流。

--不要用肉体的勤奋,去掩盖思考的懒惰--



举报

相关推荐

0 条评论