Pytest测试脚本的加载原理实质上是模块的导入原理,pytest把每个测试脚本都作为一个module进行导入,导入的模式当前支持prepend、append和importlib三种模式,默认情况下是prependd模式
一、prepend模式
Pytest默认的就是prepend模式
目录结构:
demo01/
|----demo02/
|----demo04/
|----__init__.py
|----test_demo01.py
|----demo03/
|----__init__.py
|----test_demo02.py
加载原理分析:
(1)pytest识别到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官方文档明确说明了后续将考虑把importlib模式作为默认方式