前言

我们经常会使用 pytest 去编写测试代码,也会为了使用方便去安装一些 pytest 插件,但是在使用中我们是否有关注过 pytest 是怎么执行的呢?为什么它会有这么多插件 (为什么开发它的插件这么方便)?

这里我简单分享一下 pytest 的基本执行逻辑和编写一个小插件的方法,帮助大家更好的理解 pytest 以及知道如何去扩展它

pytest 执行过程

我们首先要知道 pytest 的执行过程是怎样的,在执行 pytest 命令的时候它到底发生了什么

pytest 执行流程分为 4 个步骤:插件注册->用例收集->用例执行->报告采集

而插件注册大体又分为:内置插件定义->内置插件注册->指定插件注册->conftest 插件注册,这里的插件如果不好理解的话,可以视为执行步骤,就比如项目开发的步骤是:产品设计->开发->测试,这里的每一个步骤就是一个 pytest 的插件/hook 函数

这里给出一下官方的说明

注册插件

直接看源码,当运行 pytest.main() 时,pytest 首先去把传入的配置信息生成了一个 config 类,传入的插件信息生成 PytestPluginManager 类,然后进行内置插件的定义导入和注册和内置模块的导入

之后是注册用户提供的插件,插件的注册是 LIFO 原则,后进先执行

插件注册完成,把 session 状态修改为 start,可以看到在这里就已经通过 config.hook 调用了刚才注册过的函数,而之后的全部执行都是在通过 config.hook 里注册的函数去完成,从注册插件到生成报告的整个过程就是通过原本定义好的插件(步骤)去执行,也就是我们是可以通过传入自定义的插件,去修改 pytest 执行的逻辑的

收集用例

插件注册后,就是要进行用例收集和执行了,我们可以通过_mian 函数里的内容很清晰的知道,用例在这里通过 config.hook.pytest_collection 和 config.hook.pytest_runtestloop 两个方法去进行用例的收集和执行

说收集之前这里需要告知一个概念,在我们执行 pytest 的时候,是会默认生成一个 Session 类的实例的,这个 session 会有一个核心方法 collect(),这个方法的作用是去收集到要执行的 py 文件,这些收集到的 py 文件里的数据,会成为一个 Module 类的实例,而这个 Module 也有一个核心方法 collect(),这个方法会去收集到具体要执行的用例,使用这些用例的信息去生成 Iiem 类的实例

先看对 py 文件的收集,可以通过源码看到这个方法最后已经返回了 item 实例了,item 是执行了 genitems 方法的获取到,这个方法需要传入 rep 这个参数,也就是这里的 rep 应该就是一个 Module 实例,那我们从 collect_one_node() 这个方法找下去

最后发现发现传入的 Session 实例是执行了它的 collect() 方法,通过这个方法获取到了 py 文件的内容

继续看 session 的 collect 的内容,它调用了_collect,而_collect 中有一个看上去就像是在进行文件解析和收集的函数_collectfile,这个函数是继承自 FSCollector 类的,这个继承自 FSCollector 的方法,其实就是返回了 pytest_collect_file() 的收集结果

好,那现在我们文件已经收集了,接下来就是看一下获取用例的 genitems 方法是做了什么

这里我们发现有一个判断在,第一次我们传入的类的类型不是 nodes.Item,因此 collect_one_node 方法又执行了一次,上面已经看到了在 collect_one_node 执行的过程中,有一个步骤是需要执行类的 collect 函数的,这里 nodes.Module 是使用了父类 (PyCollector) collect 方法,而这个方法最后执行了 pytest_pycollect_makeitem 去返回了一个 item 类,最后通过 yield 向上返回(这里的 pytest_itemcollected() 方法,是只有定义没有实现的)

用例执行

执行就相对简单了,这里直接上代码

可以看到就是执行了 item 的 runtest 方法

报告采集

这个部分实际上在用例执行的过程中,报告就同步采集了,包括前面的收集等步骤的过程都是有进行报告的采集的,所以这里不细说,因为这个过程比较难拆出来单独讲,采集后的报告最后通过 pytest_terminal_summary 方法输出

以上就是基本的 pytest 运行的流程

自定义开发

现在在知道了 pytest 的执行逻辑后,我们要如何制作自己的 pytest 插件或者搞点更多的东西呢

制作插件

通过上面的描述可以知道,其实 pytest 就是在按照定义好的 hook 执行,然后因为插件的注册顺序,我们提供的插件会先执行,那其实做一个插件的方法就是去重新实现一个 hook 函数,然后让 pytest 加载进去,至于有多少可以使用的 hook 函数,不同 hook 函数的执行顺序是怎样的,可以去查看官方文档

https://www.osgeo.cn/pytest/reference.html?highlight=pytest_runtest_logreport#hooks

这里用一个 hook 举例怎么用,比如我们目前想实现一个功能,作用是及时打出错误信息,而不是运行完成后统一打出,类似 pytest-instafail,可以实现这个功能的 hook 很多,我用一个 hook 举例:

pytest_runtest_logreport(report)

我们可以看到这个 hook 有一个参数是 report,因为实际上我们的函数就是运行在 pytest 里的,这里的入参我们是可以真正拿到,这个参数是一个 TestReport 类,类的结构如下:

运行过程中,如果用例失败,失败信息的会保存在 report.longreprtext 里,所以我们这里可以在执行结束后,直接去获取这个结果,同时我们只需要测例执行时的错误输出(setup,teardown 不输出),那代码如下

/conftest.py

def pytest_runtest_logreport(report):
    if report.when == 'call':
        print(report.longreprtext)

我们把这个函数放到 conftest 里,然后执行 pytest 就可以看到效果了,这其实就是一个小插件,如果不想把插件写在 conftest 里,可以自己起一个新的 py 文件

/plugin/myplugin.py

class MyPlugin(object):

    def pytest_runtest_logreport(report):
      if report.when == 'call':
        print(report.longreprtext)

然后执行的时候这样去调用

from plugin.myplugin import MyPlugin
pytest.main(file,plugins=[MyPlugin()])

制作自动化框架

这里通过一个官方例子说明,这是官方文档上给出的一个把用例用 yaml 组织并通过 pytest 调用的例子

很明确的可以看到,这里就是先通过 pytest_collect_file 返回了一个 file 的子类(Module 类也是一个 file 的子类),然后这个类有一个 collect 方法,这个方法又返回了一个 item 子类,子类里带有 runtest 方法,整个实现逻辑就是上方说到了 pytest 的解析用例的逻辑

附加:

而如果我们现在要进行 setup,teardown 等操作的制定,这里也可通过 pytest_runtest_setup 或者给 item 子类新增一个 setup 等属性去做,但是,只建议用其中一个方法

为什么只建议用其中一个方法呢,是因为 hook 函数的注册是 LIFO 的原则,先进的函数还是会执行的,这里如果你定义了一个 pytest_runtest_setup,又定义了一个 self.setup(源码的 pytest_runtest_setup 里就是会执行一次 item.setup() 函数),实际上二者都是会执行一次的,为了避免执行内容冲突,因此只建议用一个方式去定义


↙↙↙阅读原文可查看相关链接,并与作者交流