Pytest脚本的加载原理

【原文链接】Pytest脚本的加载原理


Pytest测试脚本的加载原理实质上是模块的导入原理,pytest把每个测试脚本都作为一个module进行导入,导入的模式当前支持prepend、append和importlib三种模式,默认情况下是prepend模式

一、prepend模式

Pytest默认的就是prepend模式,下面以如下的目录结构详细的解析prepared模式下pytest脚本的加载原理。

demo01/
  |----demo02/
         |----demo04/
                |----__init__.py
                |----test_demo01.py
  |----demo03/
         |----__init__.py
         |----test_demo02.py

加载原理分析:

(1)pytest识别到test_demo01.py文件后,从当前位置开始向上递归的找带__init__.py文件的目录,直到找不到不止,比如这里就是demo04,因为demo02中没有__init__.py,因此从test_demo01.py开始找到的最上层的带有__init__.py文件的目录是demo04

(2)pytest此时把demo04的上一层目录,即demo02的目录路径插入到sys.path的开头,prepend就是表示从头插入。

(3)然后开始计算导入模块的相对路径,比如这里是demo04.test_demo01

(4)将此模块导入,然后加入到sys.modeules中,sys.modules是一个字典,key为相对路径,比如这是demo04.test_demo01,value是其对应的模块对象

(5)pytest继续识别到test_demo02.py文件,同样的原理此时找到demo03就是最顶层的带__init__.py的目录,然后把demo03的上一层目录,即demo01的目录插入到sys.path的头

(6)同理,此时导入模块后将demo03.test_demo02加入到sys.modules中

至此pytest就把测试用例加载完成了

test_demo01.py和test_demo02.py内容均如下,这里为了演示加载原理,增加了打印sys.path和sys.modules的内容

import sys

print(f"sys.path:{sys.path}")
for elem in sys.modules.keys():
    if "demo" in elem:
        print(f"module:{elem}")

def test_func():
    assert 1==1

执行结果如下,从下面的执行结果可以看出,pytest首先把’G:\src\blog\tests\demo01\demo02’ 插入到sys.path的第一个元素,然后把demo04.test_demo01 模块写入到sys.modeules中,紧接着又把’G:\src\blog\tests\demo01’插入到sys.path的第一个元素,然后又把demo03.test_demo02插入到sys.modules中,与上述分析过程完全一致

$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: G:\src\blog\tests
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collecting ... sys.path:['G:\\src\\blog\\tests\\demo01\\demo02', 'D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\python39\\
lib', 'D:\\python39', 'D:\\python39\\lib\\site-packages']
module:demo04
module:demo04.test_demo01
sys.path:['G:\\src\\blog\\tests\\demo01', 'G:\\src\\blog\\tests\\demo01\\demo02', 'D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs'
, 'D:\\python39\\lib', 'D:\\python39', 'D:\\python39\\lib\\site-packages']
module:demo04
module:demo04.test_demo01
module:demo03
module:demo03.test_demo02
collected 2 items                                                                                                                                                       

demo01\demo02\demo04\test_demo01.py .
demo01\demo03\test_demo02.py .

========================================================================== 2 passed in 0.08s ===========================================================================

二、append模式

append模式整个流程与prepend模式是完全一样的,唯一的区别就是在将找到的目录插入到sys.path的时候,append是插入到sys.path的末尾,prepend是插入到sys.path的开头

可以通过import-mode=append来指定导入模式为append,执行结果如下,可以看出,这里路径已经插入到sys.path的末尾了,这一点与prepend是不同的

$ pytest -s --import-mode=append
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: G:\src\blog\tests
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collecting ... sys.path:['D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\python39\\lib', 'D:\\python39', 'D:\\python39\\lib
\\site-packages', 'G:\\src\\blog\\tests\\demo01\\demo02']
module:demo04
module:demo04.test_demo01
sys.path:['D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\python39\\lib', 'D:\\python39', 'D:\\python39\\lib\\site-packages
', 'G:\\src\\blog\\tests\\demo01\\demo02', 'G:\\src\\blog\\tests\\demo01']
module:demo04
module:demo04.test_demo01
module:demo03
module:demo03.test_demo02
collected 2 items                                                                                                                                                       

demo01\demo02\demo04\test_demo01.py .
demo01\demo03\test_demo02.py .

========================================================================== 2 passed in 0.04s ===========================================================================

三、prepend和append模式存在的问题

prepend和append模式都存在一个问题,那就是要保持导入模块的唯一性,解释这个问题钱先看一个例子

目录结构如下:

demo01/
  |----demo02/
         |----demo04/
                |----__init__.py
                |----test_demo01.py
  |----demo04/
         |----__init__.py
         |----test_demo01.py

首先根据上面的导入原理分析一下,这里可以很容易地分析出,不论是prepend模式还是append模式,最终两个test_demo01.py要导入的模块名都是 demo01.test_demo01,在导入这两个模块后,将他们写入sys.modules时肯定是会报错的,因为sys.modules是一个字典类型的,字典类型的key是不允许重复的

两个test_demo01.py的代码均如下:

import sys

print(f"sys.path:{sys.path}")
for elem in sys.modules.keys():
    if "demo" in elem:
        print(f"module:{elem}")

def test_func():
    assert 1==1

执行结果如下,与上述分析结果是一致的,换言之,如果执行pytest的时候出现了如下错误,那么错误原因就是这个导入模块重名了

pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: G:\src\blog\tests
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collecting ... sys.path:['G:\\src\\blog\\tests\\demo01\\demo02', 'D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\python39\\
lib', 'D:\\python39', 'D:\\python39\\lib\\site-packages']
module:demo04
module:demo04.test_demo01
collected 1 item / 1 error                                                                                                                                              

================================================================================ ERRORS ================================================================================
____________________________________________________________ ERROR collecting demo01/demo04/test_demo01.py _____________________________________________________________
import file mismatch:
imported module 'demo04.test_demo01' has this __file__ attribute:
  G:\src\blog\tests\demo01\demo02\demo04\test_demo01.py
which is not the same as the test file we want to collect:
  G:\src\blog\tests\demo01\demo04\test_demo01.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
======================================================================= short test summary info ========================================================================
ERROR demo01/demo04/test_demo01.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
=========================================================================== 1 error in 0.16s ===========================================================================

解决这个问题比较简单的一个方法就是,在每个文件夹中都加一个__init__.py文件,如下

目录结构

demo01/
  |----__init__.py
  |----demo02/
         |__init__.py
         |----demo04/
                |----__init__.py
                |----test_demo01.py
  |----demo04/
         |----__init__.py
         |----test_demo01.py

这样一来继续分析一下,第一个test_demo01.py往上找,发现demo01是最后一个带__init__.py的文件夹,则把demo01的上一层目录加入到sys.path,此时第一个test_demo01.py的导入模块就变为 demo01.demo02.demo04.test_demo01,同理第二个test_demo01.py的导入模块就变为demo01.demo04.test_demo01,这样就解决了这个问题

也正是这个原因,许多文章或者教程中说pytest要求文件夹必须带__init__.py,甚至有的宣称如果不加__init__.py是不会被识别的,这个是不准确的,看到这里应该都清除这里面的本质原因了,因此,为了减少麻烦,可以保持新建文件夹都直接带上__init__.py文件保证不会出这个问题的

四、importlib模式

importlib模式是pytest6.0以后的版本支持的新的方式,importlib方式不再需要修改sys.path和sys.modules,因此不存在上面prepend和append面临的潜在的问题,采用的是一种全新的导入方式,这里首先也来看个例子

目录结构

demo01/
  |----demo02/
         |----demo04/
                |----test_demo01.py
  |----demo04/
         |----test_demo01.py

如果按照prepend或者append的思路分析,这里肯定是执行不起来的,导入模块的名字肯定是重复的,这里也可以执行以下如下:

$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: G:\src\blog\tests
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collecting ... sys.path:['G:\\src\\blog\\tests\\demo01\\demo02\\demo04', 'D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\py
thon39\\lib', 'D:\\python39', 'D:\\python39\\lib\\site-packages']
module:test_demo01
collected 1 item / 1 error                                                                                                                                              

================================================================================ ERRORS ================================================================================
____________________________________________________________ ERROR collecting demo01/demo04/test_demo01.py _____________________________________________________________
import file mismatch:
imported module 'test_demo01' has this __file__ attribute:
  G:\src\blog\tests\demo01\demo02\demo04\test_demo01.py
which is not the same as the test file we want to collect:
  G:\src\blog\tests\demo01\demo04\test_demo01.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
======================================================================= short test summary info ========================================================================
ERROR demo01/demo04/test_demo01.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
=========================================================================== 1 error in 0.16s ===========================================================================

但是因为importlib模式不会去修改sys.paht和sys.mo,因此也就不会有这个问题了,执行结果如下:

$ pytest -s --import-mode=importlib
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.6, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: G:\src\blog\tests
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collecting ... sys.path:['D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\python39\\lib', 'D:\\python39', 'D:\\python39\\lib
\\site-packages']
sys.path:['D:\\python39\\Scripts\\pytest.exe', 'D:\\python39\\python39.zip', 'D:\\python39\\DLLs', 'D:\\python39\\lib', 'D:\\python39', 'D:\\python39\\lib\\site-packages
']
collected 2 items                                                                                                                                                       

demo01\demo02\demo04\test_demo01.py .
demo01\demo04\test_demo01.py .

========================================================================== 2 passed in 0.06s ===========================================================================

这就是pytest自动化脚本的加载原理,至此也就明白当前pytest默认情况下采用的是prepared模式,而在这种模式下,如果文件夹中没有__init__.py文件,一定要保持测试文件命名的独一无二性,所以在实践中,为了减少一些潜在的问题,建议在创建文件夹的时候,直接在所有文件夹下创建__init__.py文件,如此则不需要担心测试脚本文件名重名的问题了。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

redrose2100

您的鼓励是我最大的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值