pytest parameterization: a powerful tool to simplify test case writing

In the actual scenario, we test the simple registration function, which requires a username and password. The username/password may have some rules, which requires a variety of different rules of data to verify the registration function. Of course, we can write multiple cases, the requests are the same but the request data is different. But there is a problem with this, it will cause a lot of duplicate code and is not easy to manage. So how to solve it elegantly? Of course it is parameterization, so how does pytest perform parameterization? Explore together with questions.

Introduction to pytest parameterization

Parameterized testing refers to running multiple tests by passing in different parameters in the test case to verify the different input and output of the function or method under test.

Pytest parameterization allows us to easily expand test cases, reduce redundant code, and improve testing efficiency.

How to use pytest parameterization

The method of use is still very simple. Let’s look at a case first. Maybe you will understand it at a glance.

@pytest.mark.parametrize("input, expected", [
    (twenty four),
    (3, 9),
    (4, 16)
])
def test_square(input, expected):
    assert input**2 == expected

The above code defines a test case named test_square, which is parameterized using the @pytest.mark.parametrize decorator. The parameterized parameter list contains multiple sets of inputs and desired outputs, separated by commas between each set of parameters.

Next we execute the test and see the results:

Run the test by executing the following command on the command line:

============================== test session starts =============== ===============
collecting...collected 3 items
?
test_demo.py::test_square[2-4]
test_demo.py::test_square[3-9]
test_demo.py::test_square[4-16]
?
============================== 3 passed in 0.13s =============== ================
?

Application scenarios

The usage method is just like the above case, it is very simple. Let’s focus on the application scenario.

Single parameterized application

Common usage scenarios: Only one data changes in the test method, that is, multiple sets of test data are passed in through a parameter. During execution, each set of data is executed once.

For example, if we register for verification code, there are currently multiple mobile phone numbers that need to be registered:

import pytest
?
?
@pytest.mark.parametrize("mobile", [
    16300000000,
    16300000001,
    16300000002
])
def test_register(mobile):
    print(f"The current registered mobile phone number is: {mobile}")

The execution results are as follows:

PASSED [33%]The current registered mobile phone number is: 16300000000
PASSED [66%]The current registered mobile phone number is: 16300000001
PASSED [100%]The current registered mobile phone number is: 16300000002

It can be seen that a test case will be executed multiple times if it has multiple pieces of data.

Multi-parameter application

The input data for the test can be an expression, and the input parameters can be multiple. Multiple data can be organized in tuples.

For example, if we test the calculation function, we can write it like this:

import pytest
?
?
@pytest.mark.parametrize("input, expected", [
    (2 + 2, 4),
    (10-1, 9),
    (4**2, 16)
])
def test_square(input, expected):
    print(input)
    assert input == expected

This code performs different calculation operations on input values and verifies that the results are as expected.

Multiple parameterization

A use case can be marked using multiple @pytest.mark.parametrizes. For example:

import pytest
?
?
@pytest.mark.parametrize('input', [1, 2, 3])
@pytest.mark.parametrize("output, expected", [
    (4, 5),
    (6, 7)
])
def test_square(input, output, expected):
    print(f"input:{input},output:{output},expected:{expected}")
    result = input + output
    assert result == expected

We use two levels of nested @pytest.mark.parametrize decorators. The outer parameterized decorator specifies the value range of the input parameter as [1, 2, 3], and the inner parameterized decorator specifies each set of values for the output and expected parameters.

The combination of parameterization and fixture

Fixtures can also be parameterized. They are introduced in detail in the previous article. I will not introduce them in this article. Students who don’t know how to do this can read this article.

Pytestmark implements parameterization

pytestmark can be used to apply decorators at the test module level or class level. By using pytestmark, we can apply parameterization uniformly for multiple test functions in the test module.

Let’s look at the case:

import pytest
?
pytestmark = pytest.mark.parametrize('input', [1, 2, 3])
?
def test_square(input):
    result = input ** 2
    assert result == 4
?

In this code, we use the pytest.mark.parametrize decorator in the module-level pytestmark variable and set the input parameter to a parameterized value of [1, 2, 3].

This means that for each test function in the test module, parameterization is applied and the test is executed for each value in [1, 2, 3]. In the test_square test function, we directly use input as a parameter to access the parameterized value, calculate the square of input, and assert that the result is 4.

By using pytestmark, we can easily apply parameterization throughout the test module without repeating the same decorator in every test function. This approach is particularly useful when the same parameterization needs to be applied uniformly to multiple test functions.

It is important to note that when using pytestmark, it will apply parameterization to all test functions in the entire module. If you only want to apply parameterization to a specific test function, you can apply the decorator directly to that function without using the pytestmark variable.

Deep dig into parametrize

Let’s take a look at the source code first: /_pytest/python.py

def parametrize(
        self,
        argnames: Union[str, Sequence[str]],
        argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
        indirect: Union[bool, Sequence[str]] = False,
        ids: Optional[
            Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
        ] = None,
        scope: "Optional[_ScopeName]" = None,
        *,
        _param_mark: Optional[Mark] = None,
    ) -> None:

argnames: parameter name, which can be a string or a list of strings, representing the parameter names in the test function. If there are multiple parameters, multiple names can be specified via a list.

argvalues: parameter values, which can be an iterable object, where each element represents a set of parameter values. Each set of parameter values can be a tuple, list, or a single object.

indirect: Whether to use the parameter as an indirect parameter of the function under test. If set to True or specify certain parameter names, when the test function is executed, the parameters will be passed in as return values from other fixture functions rather than directly as parameter values.

ids: The identifier of the test case, used to better distinguish different parameterized test cases in the test report. You can specify an iterable or a function to generate the identity.

scope: The scope of the parameter, used when sharing parameters between multiple test functions. Can be set to “function” (default), “class”, “module” or “session”.

_param_mark: Internal parameter, used to specify parameter mark. Generally no need to pay attention.

Multiple different sets of parameterizations can be added to a test function by calling the pytest.parametrize function multiple times, with each call applying the parameterization on top of the previously added parameterization.

Note that the pytest.parametrize function is parameterized during the test case collection phase, rather than dynamically generating parameters each time a test case is executed.

Use the hook function pytest_generate_tests combined with parametrize to dynamically generate test cases.

pytest.param

As a powerful and easy-to-use testing framework, Pytest provides the @pytest.mark.parametrize decorator to support parameterized testing. The pytest.param function is a tool that further enhances parameterized testing, allowing us to define and control parameterized test cases in a more flexible way.

Parameterized identifier

In parametric testing, each test case may contain multiple sets of parameters and may produce a large number of test results. At this point, in order to better understand and debug the test results, it makes sense to assign an easy-to-understand identifier to each parameterized test case. The id parameter of the pytest.param function can do this.

For example, in a multiplication test, we can define a parameterized test case as follows:

import pytest
?
@pytest.mark.parametrize("input, expected", [
    pytest.param(2, 4, id="case1"),
    pytest.param(3, 9, id="case2"),
    pytest.param(5, 25, id="case3")
])
def test_multiply(input, expected):
    assert input * input == expected

By using pytest.param for each parameterized test case, we can specify an identifier for each test case, making it easier to read and understand. When the test run is completed, the identification in the test report will help us better locate the problem.

Custom options

In addition to parameter identification, the pytest.param function can also accept additional parameters, such as the marks parameter, which is used to apply custom marks for individual test cases. By using the marks parameter, we can flexibly add various customization options in parameterized tests.

For example, assuming we have a parameterized test case that needs to be skipped, we can define it like this:

import pytest
?
@pytest.mark.parametrize("input, expected", [
    pytest.param(2, 4, marks=pytest.mark.skip),
    pytest.param(3, 9, marks=pytest.mark.skip),
    pytest.param(5, 25)
])
def test_multiply(input, expected):
    assert input * input == expected

In the above example, we use the pytest.mark.skip mark to skip the first two test cases. In this way, we can apply different marks to different parameterized test cases, such as pytest.mark.skip, pytest.mark.xfail, etc., to achieve more flexible test control.

Finally

Through the introduction of this article, we have learned about the concept, usage and case demonstration of pytest parameterization. Pytest parameterization can greatly simplify the writing of test cases and improve the reusability and maintainability of the code. In actual software testing, reasonable use of pytest parameterization will help us test more efficiently and find potential problems faster. In addition, we also introduced the advanced usage of parametrize, which can be combined with the hook function pytest_generate_tests to implement complex scenarios. Finally we introduced a very useful function pytest.param, which provides more customization options and control for parameterized tests. Therefore, when writing parameterized tests, you may wish to consider using pytest.param to improve the quality and efficiency of your tests.

Finally: The complete software testing video tutorial below has been compiled and uploaded. Friends who need it can get it by themselves [Guaranteed 100% Free]

Software testing interview document

We must study to find a high-paying job. The following interview questions are from the latest interview materials from first-tier Internet companies such as Alibaba, Tencent, Byte, etc., and some Byte bosses have given authoritative answers. After finishing this set I believe everyone can find a satisfactory job based on the interview information.