Using pytest to play with data-driven testing framework

Posted by jmandas on Sat, 01 Jan 2022 19:03:48 +0100

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:

  1. How does pytest collect use cases?
  2. 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:

  1. Build Package
  2. Build Module
  3. 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

Topics: Python software testing Testing