6

【pytest官方文档】解读- 插件开发之hooks 函数(钩子) - 把苹果咬哭的测试笔记

 1 year ago
source link: https://www.cnblogs.com/pingguo-softwaretesting/p/16782158.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

一、什么是 hooks 函数

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

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

1268169-20221011203406416-1798893598.png

二、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

1268169-20221013195737846-819755231.png

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

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

3. hook函数中的 hookwrapper

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

1268169-20221013200339760-1594545452.png

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

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

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

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

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

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

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

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

1268169-20221013202739195-1666297591.png

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

1268169-20221013202818487-1815708584.png

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

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

# 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. Plugin3pytest_collection_modifyitems一直调用到yield,因为它是一个钩子包装器。
  2. Plugin1pytest_collection_modifyitems被调用,因为它被标记为tryfirst=True
  3. Plugin2pytest_collection_modifyitems被调用,因为它被标记为trylast=True(但即使没有这个标记,它也会在Plugin1之后)。
  4. Plugin3pytest_collection_modifyitems继续在yield执行代码,yield接收一个Result实例。

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK