This article is selected from the tester community
What is the pytest architecture?
First, let's look at an example of pytest:
def test_a(): print(123)
collected 1 item test_a.py . [100%] ============ 1 passed in 0.02s =======================
The output result is simple: one test case is collected and the test case is executed successfully.
At this point, consider two questions:
- How does pytest collect use cases?
- How does pytest convert python code into pytest test test cases (also known as item s)?
How does pytest collect use cases?
This is very simple. Traverse the execution directory. If you find that there are python objects that meet the requirements of "pytest test test cases" in the modules of the directory, convert them into pytest test test cases.
For example, write the following hook functions:
def pytest_collect_file(path, parent): print("hello", path)
hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\__init__.py hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\conftest.py hello C:\Users\yuruo\Desktop\tmp\tmp123\tmp\testcase\test_a.py
You will see all the file contents.
How to construct the item of pytest?
pytest is like a box that wraps python objects, as shown in the following figure:
When writing python code:
def test_a: print(123)
Will be wrapped into Function:
<Function test_a>
You can view the details from the hook function:
def pytest_collection_modifyitems(session, config, items): pass
Therefore, understanding the package process is the key to solving the puzzle. How does pytest wrap python objects?
The following code has only two lines. It seems simple, but it contains mystery!
def test_a: print(123)
Cut the code position into a diagram as follows:
We can say that the above code is the "test_a function" of the "test_a.py module" under the "testcase package", and the test cases generated by pytest should also have this information:
"Test_a" test case of "test_a.py module" under "testcase package":
Convert the above expression into the following figure:
pytest uses the parent attribute to represent the upper layer level relationship. For example, Module is the parent of Function, and the parent attribute of Function is as follows:
<Function test_a>: parent: <Module test_parse.py>
Of course, the parent of the Module is the Package:
<Module test_parse.py>: parent: <Package tests>
Note the case: module is a class of pytest, which is used to wrap the module of python. Module and module have different meanings.
Here's a general introduction. python's package and module are real objects. You can see from the obj attribute, such as the obj of module
The properties are as follows:
If you understand the package purpose of pytest, it's very good! Let's discuss the next step: how to construct the item of pytest?
Take the following code as an example:
def test_a: print(123)
To construct the item of pytest, you need to:
- Build Package
- Build Module
- Build Function
Take building a Function as an example, you need to call its from_ The parent () method is used to build. The process is shown in the following figure:
From Function name from_parent, you can guess that "build Function" must have a lot to do with its parent! And because of the Function's parent
Yes Module: according to the partial code of the following Function (located in the python.py file):
class Function(PyobjMixin, nodes.Item): # Used to create test cases @classmethod def from_parent(cls, parent, **kw): """The public constructor.""" return super().from_parent(parent=parent, **kw) # Get instance def _getobj(self): assert self.parent is not None return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] # Run test cases def runtest(self) -> None: """Execute the underlying test function.""" self.ihook.pytest_pyfunc_call(pyfuncitem=self)
It is concluded that you can use Module to build Function! The calling pseudo code is as follows:
Function.from_parent(Module)
Since you can use Module to build functions, how do you build modules?
Of course, the Module is built using Package!
Module.from_parent(Package)
Since you can use Package to build modules, how do you build packages?
Don't ask. It's almost a doll. Please see the call relationship in the figure below:
Pytest starts from Config and builds layer by layer until function! Function is the smallest execution unit of pytest.
How to build an item manually?
Manual build item It's simulation pytest structure Function The process of. That is, you need to create Config ,Then use Config establish Session ,Then use Session establish Package ,...,Finally create Function.
In fact, it is not so complicated. pytest will automatically create Config, Session and Package, which do not need to be created manually.
For example, write the following hook code and break to view its parent parameter:
def pytest_collect_file(path, parent): pass
If the traversed path is a package (you can view the specific path from the path parameter), such as the package in the following figure:
The parent parameter is Package, which can be used to create a Module:
Write the following code to build the module of pytest. If it is found to be a yaml file, dynamically create the module and module according to the contents of the yaml file:
from _pytest.python import Module, Package def pytest_collect_file(path, parent): if path.ext == ".yaml": pytest_module = Module.from_parent(parent, fspath=path) # Return the self-defined python module pytest_module._getobj = lambda : MyModule return pytest_module
It should be noted that the above code is rewritten with monkey patch_ getobj method, why?
Module utilization_ The getobj method finds and imports (import statement) the module under the path package. Its source code is as follows:
# _pytest/python.py Module class Module(nodes.File, PyCollector): def _getobj(self): return self._importtestmodule() def _importtestmodule(self): # We assume we are only called once per module. importmode = self.config.getoption("--import-mode") try: # Key code: import module from path mod = import_path(self.fspath, mode=importmode) except SyntaxError as e: raise self.CollectError( ExceptionInfo.from_current().getrepr(style="short") ) from e # Omit some code
However, if you use data-driven, that is, the user created data file test_parse.yaml, it's not python files will not be recognized as module s by py thon (only
. py file can be recognized as module).
At this time, pytest cannot import (import statement) test_parse.yaml. You need to dynamically rewrite _getobj and return a custom module!
Therefore, you can use lambda expressions to return custom module s:
lambda : MyModule
How to customize a module
This involves metaprogramming Technology: dynamically build python modules and dynamically add classes or functions to modules:
import types # Create module dynamically module = types.ModuleType(name) def function_template(*args, **kwargs): print(123) # Add function to module setattr(module, "test_abc", function_template)
To sum up, put the self-defined module into the module of pytest to generate an item:
# conftest.py import types from _pytest.python import Module def pytest_collect_file(path, parent): if path.ext == ".yaml": pytest_module = Module.from_parent(parent, fspath=path) # Create module dynamically module = types.ModuleType(path.purebasename) def function_template(*args, **kwargs): print(123) # Add function to module setattr(module, "test_abc", function_template) pytest_module._getobj = lambda: module return pytest_module
Create a yaml file and run pytest:
======= test session starts ==== platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: C:\Users\yuruo\Desktop\tmp plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1 collected 1 item test_a.yaml 123 . ======= 1 passed in 0.02s ===== PS C:\Users\yuruo\Desktop\tmp>
Now stop and review what we did?
Borrow pytest hook to Convert yaml file to python module.
What did we do as a data-driven testing framework?
Failed to parse yaml file content! The functions in the module generated above are as follows:
def function_template(*args, **kwargs): print(123)
Just a simple print 123. The data-driven testing framework needs to parse yaml content and dynamically generate functions or classes according to the content. For example, the following yaml contents:
test_abc: - print: 123
The meaning of the expression is "define the function test_abc, which prints 123".
Note: the meaning of keywords should be decided by you. Here is only a demo demonstration!
You can use yaml safe_ Load loads yaml content and performs keyword parsing, where path Strpath represents the address of yaml file:
import types import yaml from _pytest.python import Module def pytest_collect_file(path, parent): if path.ext == ".yaml": pytest_module = Module.from_parent(parent, fspath=path) # Create module dynamically module = types.ModuleType(path.purebasename) # Parsing yaml content with open(path.strpath) as f: yam_content = yaml.safe_load(f) for function_name, steps in yam_content.items(): def function_template(*args, **kwargs): """ Function module """ # Traverse multiple test steps [print: 123, print: 456] for step_dic in steps: # Parse a test step print: 123 for step_key, step_value in step_dic.items(): if step_key == "print": print(step_value) # Add function to module setattr(module, function_name, function_template) pytest_module._getobj = lambda: module return pytest_module
The operation results of the above test cases are as follows:
=== test session starts === platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: C:\Users\yuruo\Desktop\tmp plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1 collected 1 item test_a.yaml 123 . === 1 passed in 0.02s ====
Of course, some complex test cases are also supported:
test_abc: - print: 123 - - print: 456 - test_abd: - - print: 123 - - print: 456
The results are as follows:
== test session starts == platform win32 -- Python 3.8.1, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 rootdir: C:\Users\yuruo\Desktop\tmp plugins: allure-pytest-2.8.11, forked-1.3.0, rerunfailures-9.1.1, timeout-1.4.2, xdist-2.2.1 collected 2 items test_a.yaml 123 456 .123 456 . == 2 passed in 0.02s ==
Using pytest to create a data-driven testing framework is introduced here. I hope it can bring you some help. You can also leave a message to discuss what you don't understand or have doubts. Let's make progress together!
** _
Come to Hogwarts test and development society to learn more advanced technologies of software testing and test development. The knowledge points include web automated testing, app automated testing, interface automated testing, test framework, performance testing, security testing, continuous integration / continuous delivery / DevOps, test left, test right, precision testing, test platform development, test management, etc, The course technology covers bash, pytest, junit, selenium, appium, postman, requests, httprunner, jmeter, jenkins, docker, k8s, elk, sonarqube, Jacobo, JVM sandbox and other related technologies, so as to comprehensively improve the technical strength of test and development engineers
QQ communication group: 484590337
The official account TestingStudio
Video data collection: https://qrcode.testing-studio.com/f?from=CSDN&url=https://ceshiren.com/t/topic/15844
Click for more information