Loading principle of Pytest script

[Original link] Pytest script loading principle


The principle of loading Pytest test scripts is essentially the import principle of modules. pytest imports each test script as a module. The import mode currently supports three modes: prepend, append and importlib. By default, it is the prepend mode.

1. prepend mode

The default mode of pytest is the prepend mode. The following directory structure is used to analyze the loading principle of the pytest script in the prepared mode in detail.

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

Loading principle analysis:

(1) After pytest recognizes the test_demo01.py file, it recursively looks for the directory with the __init__.py file from the current location until it cannot find it. For example, here is demo04, because there is no __init__.py in demo02, so The topmost directory with the __init__.py file found from test_demo01.py is demo04

(2) At this time, pytest inserts the upper directory of demo04, that is, the directory path of demo02, to the beginning of sys.path, and prepend means inserting from the beginning.

(3) Then start to calculate the relative path of the imported module, for example, here is demo04.test_demo01

(4) Import this module, and then add it to sys.modules, sys.modules is a dictionary, key is a relative path, for example, this is demo04.test_demo01, and value is its corresponding module object

(5) pytest continues to recognize the test_demo02.py file. The same principle finds that demo03 is the top-level directory with __init__.py, and then inserts the upper-level directory of demo03, that is, the directory of demo01, into sys.path head

(6) Similarly, add demo03.test_demo02 to sys.modules after importing the module

So far, pytest has loaded the test cases

The contents of test_demo01.py and test_demo02.py are as follows. Here, in order to demonstrate the loading principle, the contents of printing sys.path and sys.modules are added.

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

The execution results are as follows. As can be seen from the following execution results, pytest first inserts 'G:\src\blog\tests\demo01\demo02' into the first element of sys.path, and then writes the demo04.test_demo01 module into In sys.modules, insert 'G:\src\blog\tests\demo01' into the first element of sys.path, and then insert demo03.test_demo02 into sys.modules, which is exactly the same as the above analysis process unanimous

$ 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 ===========================================================================

Two, append mode

The whole process of append mode is exactly the same as that of prepend mode. The only difference is that when inserting the found directory into sys.path, append is inserted at the end of sys.path, and prepend is inserted at the beginning of sys.path.

You can use import-mode=append to specify the import mode as append. The execution results are as follows. It can be seen that the path here has been inserted at the end of sys.path, which is different from 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 ===========================================================================

3. Problems with prepend and append modes

There is a problem in both prepend and append modes, that is, to maintain the uniqueness of imported modules. To explain this problem, let’s first look at an example.

The directory structure is as follows:

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

First, analyze it according to the import principle above. It can be easily analyzed here that whether it is prepend mode or append mode, the final module names of the two test_demo01.py to be imported are demo01.test_demo01. After importing these two modules, When writing them into sys.modules, an error will definitely be reported, because sys.modules is a dictionary type, and the key of the dictionary type is not allowed to be repeated

The codes of the two test_demo01.py are as follows:

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

The execution results are as follows, which is consistent with the above analysis results. In other words, if the following error occurs when executing pytest, the cause of the error is that the imported module has the same name.

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 ===========================================================================

A relatively simple way to solve this problem is to add an __init__.py file in each folder, as follows

Directory Structure

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

In this way, continue to analyze, look up the first test_demo01.py, and find that demo01 is the last folder with __init__.py, then add the upper directory of demo01 to sys.path, at this time the first The import module of test_demo01.py becomes demo01.demo02.demo04.test_demo01. Similarly, the import module of the second test_demo01.py becomes demo01.demo04.test_demo01, which solves this problem

It is also for this reason that many articles or tutorials say that pytest requires that the folder must have __init__.py, and some even claim that if __init__.py is not added, it will not be recognized. This is not accurate, see here The essential reasons should be cleared up. Therefore, in order to reduce trouble, you can keep the __init__.py file directly in the new folder to ensure that this problem will not occur.

Four, importlib mode

The importlib mode is a new method supported by versions after pytest6.0. The importlib method no longer needs to modify sys.path and sys.modules, so there is no potential problem faced by the above prepend and append, and a new import is adopted way, let’s look at an example first

Directory Structure

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

If you analyze it according to the idea of ​​prepend or append, it must not be executed here, and the name of the imported module must be repeated. Here, you can also execute the following:

$ 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 ===========================================================================

But because the importlib mode will not modify sys.paht and sys.mo, so there will be no such problem. The execution results are as follows:

$ 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 ===========================================================================

This is the loading principle of the pytest automation script. So far, it is understood that the current pytest adopts the prepared mode by default. In this mode, if there is no __init__.py file in the folder, the test file must be named uniquely. Therefore, in practice, in order to reduce some potential problems, it is recommended to create __init__.py files directly under all folders when creating folders, so that you don’t need to worry about the problem of duplicate test script file names.

Guess you like

Origin blog.csdn.net/redrose2100/article/details/122166965