This article will divide the content about Pytest into two parts. The first part mainly involves the introduction of Pytest concepts and functional components, and the second part mainly uses Pytest in practice as a Web project. \

Why do unit tests

Many Python users will have had the experience of testing whether a module or function produces the desired result by printing the part of the output to the console using the print() function.

def myfunc(*args, **kwargs):
    do_something()
    data = ...
    print(data)
Copy the code

In the process of improvement, print() function is often used to ensure the accuracy of the results. However, due to the increasing number of modules or functions to be tested, various unremoved or annotated print() calls are gradually left in the code, making the whole code not so concise and decent.

In programming, there is often the concept of “unit testing”, which refers to the examination and verification of the smallest testable units in software. This minimum measurable unit can be any or a combination of our expressions, functions, classes, modules, packages, so we can uniformly put the steps we tested with print() into a unit test.

Python already has a built-in unitTest module for unit testing. For starters, unitTest is a bit of a learning curve because it is encapsulated by inheriting TestCase classes, so you need to know enough about object orientation. Binding to classes means that if you want to achieve customization or module decoupling, you may need to spend a little more time designing partitions.

So, in order to make testing simple and extensible, a testing framework called PyTest was created in the Python community. With PyTest, we don’t have to worry about how to implement our tests based on TestCase, we just need to keep our code logic as simple as possible. An assert keyword is added to assert the result, and PyTest does the rest for us.

# main.py

import pytest

raw_data = read_data(...)

def test_myfunc(*args, **kwargs):
    do_something()
    data = ...
    assert data == raw_data

if __name__ == '__main__':
    pytest.main()
Copy the code

Then we just need to run the main.py file containing the above code and see the results pyTest has tested for us on the console. If the result passes, not much information is displayed, and if the test fails, an error message is thrown and the runtime is told what is in data.

While pyTest is simple enough to say, it also provides a number of useful features (e.g., dependency injection) that have some conceptual knowledge of their own; This doesn’t mean to discourage people from using PyTest to test their code, but rather to give us more options, so it’s only with a better understanding of pyTest’s capabilities and concepts that we can fully leverage PyTest’s power.

Quickly implement your first Pytest test

With PyTest installed by PIP Install PyTest, we can quickly implement our first test.

We can create a new Python file at random, which I’ll call test_main.py, and leave the following in it:

from typing import Union

import pytest

def add(
    x: Union[int, float], 
    y: Union[int, float],
) -> Union[int, float]:
    return x + y

@pytest.mark.parametrize(
    argnames="x,y,result", 
    argvalues=[
        (1.1.2),
        (2.4.6),
        (3.3.3.6.3),
    ]
)
def test_add(
    x: Union[int, float], 
    y: Union[int, float],
    result: Union[int, float],
):
    assert add(x, y) == result
Copy the code

Pytest -v = pyTest -v = pyTest -v = pyTest -v = pyTest

You can see that we don’t have to pass the parameters through the for loop repeatedly, and you can visually see from the results what the exact values of the parameters passed in each test are. We’ll just need the Mark. Parametrize decorator provided by PyTest. Pytest is relatively easy to get started with, but we need to understand some of the concepts in the framework.

Pytest concepts and usage

named

If pyTest is required to test your code, we first need to test functions, classes, methods, modules, and even code files that start with test_* or end with *_test by default. This is to comply with standard testing conventions. If we remove test_ from the file name of the previous quickstart example, we see that PyTest does not collect the corresponding test case.

Of course, we can also change different prefixes or suffixes in the PyTest configuration file, as in the official example:

# content of pytest.ini
# Example 1: have pytest look for "check" instead of "test"
[pytest]
python_files = check_*.py
python_classes = Check
python_functions = *_check
Copy the code

But usually we just use the default test prefix and suffix. If we want to select a specific test case or test only modules under a specific module, we can specify this on the command line with a double colon, like this:

pytest test.py::test_demo
pytest test.py::TestDemo::test_demo
Copy the code

Mark

In PyTest, the Mark tag is a very useful function. It decorates the object to be tested with the tag decorator, so that PyTest will perform operations on our function based on the mark function.

The official itself provides some pre-built Mark functions, so we’ll just pick the usual ones.

Parameter test: PyTest. Parametrize

As with the previous example and its name, Mark. parametrize is primarily for scenarios where we want to pass different parameters, or different combinations of parameters, to an object to be tested.

As with our previous test_add() example, we tested separately:

  • whenx=1y=1Is, the result isresult=2In the case
  • whenx=2y=4Is, the result isresult=6In the case
  • whenX = 3.3y=3Is, the result isResult = 6.3In the case

We can also stack the parameters and combine them, but the effect is similar:

import pytest

@pytest.mark.parametrize("x"[0.1])
@pytest.mark.parametrize("y"[2.3])
@pytest.mark.parametrize("result"[2.4])
def test_add(x, y, result):
    assert add(x,y) == result
Copy the code

Of course, if we have enough parameters, all we need to do is make sure we’re parametrize, PyTest will still help us test everything. So we never have to write extra code again.

It’s important to note, however, that parametrize is a little different from the important concept of fixtures that we’ll talk about later: the former is all about modeling what objects under test will produce with different parameters, while the latter is about testing what they will produce with fixed parameters or data.

Skip the test

In some cases, our code contains sections for different situations, versions, or compatibility. The code is usually only applicable if certain conditions are met. Otherwise, there will be problems with execution. It would be unreasonable to test or fail as a use case at this point. For example, I wrote a compatibility function, add(), for Python 3.3, which is bound to be problematic when used later than Python 3.3.

To accommodate this, PyTest provides the mark.skip and Mark. skipIf tags, although the latter is more commonly used.

import pytest
import sys

@pytest.mark.skipif(sys.version_info >= (3.3))
def test_add(x, y, result):
    assert add(x,y) == result
Copy the code

So when we add this tag, every time we use the SYS module before the test case to determine if the Python interpreter version is greater than 3.3, it will automatically skip if it is.

Expected exception

As long as code is written by human beings, there will inevitably be bugs. Of course, some bugs are expected by code writers. These special bugs are also called exceptions. Let’s say we have a division function:

def div(x, y):
    return x / y
Copy the code

But according to our algorithm, the divisor can’t be zero; So if we pass y=0, we must raise a ZeroDivisionError. So the usual way to do this is either try… Exception to catch the exception and throw the corresponding error message (we can also use the if statement to determine the condition, which also throws an error) :

def div(x, y):
    try:
        return x/y
    except ZeroDivisionError:
        raise ValueError("Y cannot be zero.")
Copy the code

Therefore, during the test, if we want to test whether the exception assertion is raised correctly, we can use the raises() method provided by PyTest:

import pytest

@pytest.mark.parametrize("x"[1])
@pytest.mark.parametrize("y"[0])
def test_div(x, y):
    with pytest.raises(ValueError):
        div(x, y)
Copy the code

Note that we need to assert that we are capturing the ValueError we specified to be raised after raising ZeroDivisionError, not the ZeroDivisionError. Of course we could use another token method (PyTest.Mark.xfail) to combine this with PyTest.Mark.parametrize:


@pytest.mark.parametrize(
    "x,y,result", 
    [
        pytest.param(1.0, None, marks=pytest.mark.xfail(raises=(ValueError))),
    ]
)
def test_div_with_xfail(x, y, result):
    assert div(x,y) == result
Copy the code

This will directly mark the failed parts of the test.

Fixture

Of PyTest’s many features, the most impressive is fixtures. Fixtures most people translate the word literally as “fixtures,” but if you know anything about the Java Spring framework it’s easier to think of it in practice as something like an IoC container, but I think it’s more appropriate to call it “fixtures.”

Fixtures are often used to provide our test cases with a fixed, generic object that can be dismantled and assembled, acting as a container that holds something in it. When we use it for our unit tests, PyTest will automatically inject the corresponding objects into the vehicle.

Here I’m simulating a little bit what happens when we use a database. Typically, we create a database object from a database class, connect before use, then operate, and finally disconnect close() after use to free resources.

# test_fixture.py

import pytest

class Database(object):

    def __init__(self, database):
        self.database = database
    
    def connect(self):
        print(f"\n{self.database} database has been connected\n")

    def close(self):
        print(f"\n{self.database} database has been closed\n")

    def add(self, data):
        print(f"`{data}` has been add to database.")
        return True

@pytest.fixture
def myclient():
    db = Database("mysql")
    db.connect()
    yield db
    db.close()

def test_foo(myclient):
    assert myclient.add(1) == True
Copy the code

In this code, the key to implementing the payload is the @pytest.fixture line of decorator code that allows us to directly use a function with a resource as our payload, passing the signature (or naming) of the function as a parameter to our test case. Pytest automatically helps us inject when we run the tests.

Pytest will execute the connect() method of the db object in MyClient () for us during the injection process, and will call the close() method again for us after the test is complete to release resources.

Pytest’s fixture mechanism is the key to implementing complex testing. Imagine writing a fixture with test data that you can use multiple times in different modules, functions, or methods.

Of course, PyTest provides us with the situation of adjustable vehicle scope, from small to large:

  • function: Function scope (default)
  • class: class scope
  • module: module scope
  • package: packet scope
  • session: session scope

Vehicles are created and destroyed over the life of the scope. So if we want to increase the scope of the vehicle we’re creating, we can increase the scope of the vehicle by adding a scope parameter to @pytest.fixture().

Although PyTest officially provides us with a few built-in general vehicles, we usually have more of our own custom vehicles. So we can all manage them in a file called conftest.py:

# conftest.py

import pytest

class Database:
    def __init__(self, database):
        self.database:str = database
    
    def connect(self):
        print(f"\n{self.database} database has been connected\n")

    def close(self):
        print(f"\n{self.database} database has been closed\n")

    def add(self, data):
        print(f"\n`{data}` has been add to database.")
        return True

@pytest.fixture(scope="package")
def myclient():
    db = Database("mysql")
    db.connect()
    yield db
    db.close(a)Copy the code

Since we declared the scope to be the same package, we changed the previous test_add() part of the test slightly under the same package to inject and use the myClient vehicle without explicitly importing it:

from typing import Union

import pytest

def add(
    x: Union[int, float], 
    y: Union[int, float],
) -> Union[int, float]:
    return x + y

@pytest.mark.parametrize(
    argnames="x,y,result", 
    argvalues=[
        (1.1.2),
        (2.4.6),
    ]
)
def test_add(
    x: Union[int, float], 
    y: Union[int, float],
    result: Union[int, float],
    myclient
):
    assert myclient.add(x) == True
    assert add(x, y) == result
Copy the code

Then run Pytest-vs to see the output:

Pytest extension

As everyone who uses frameworks knows, the ecology of a framework indirectly affects how it develops (Django and Flask, for example). Pytest has plenty of room for extension and many easy-to-use features that make it possible to use plug-ins or third-party extensions.

According to the official List of plug-ins, there are about 850 plug-ins or third-party extensions in PyTest at present. We can find the Plugin List page in The official Reference of PyTest. Here I mainly select two plug-ins related to our next chapter practice:

Related plug-ins can be installed as needed and then through the PIP command, and finally use only a simple reference to the use of the plug-in documentation to write the corresponding part, and finally start pyTest test can be.

pytest-xdist

Pytest-xdist is a PyTest plug-in that is maintained by the PyTest team and allows us to do parallel testing to improve our testing efficiency, because if our project is of a certain size, there will be a lot of testing. Because PyTest collects test cases in a synchronous manner, it does not take full advantage of multiple cores.

So with Pytest-Xdist we were able to dramatically speed up each round of testing. -n

= -n

= -n

= -n

= -n




pytest-asyncio

Pytest – asyCnio is an extension that allows PyTest to test asynchronous functions or methods. Since most asynchronous frameworks or libraries are based on Python’s official Asyncio implementation, Pytest-Asyncio can further integrate asynchronous tests and asynchronous vehicles into test cases.

We can decorate an asynchronous function or method directly with the @pytest.mark.Asyncio tag in the function or method under test, and then test it:

import asyncio

import pytest


async def foo():
     await asyncio.sleep(1)
     return 1

@pytest.mark.asyncio
async def test_foo():
    r = await foo()
    assert r == 1
Copy the code

conclusion

This article gives a brief introduction to pyTest concepts and its core features, and we can see how easy PyTest is to use in the testing section. Pytest features and usage examples go much further than that, and the official documentation is comprehensive enough for those interested to take a closer look.

In the next installment, we’ll take a closer look at integrating PyTest with Web projects.

Author: 100gle, a non-serious liberal arts student who has been practicing for less than two years, likes to type code, write articles and do various new things; Now he is engaged in the related work of big data analysis and mining. \

Appreciate the author

Read more

Top 10 Best Popular Python Libraries of 2020 \

2020 Python Chinese Community Top 10 Articles \

5 minutes to master Python object references \

Special recommendation \

\

Click below to read the article and join the community