review with examples

pip install pytest

Syntax

Before moving on to parameterization, let's look at the main features of Pytest:

  • Automatic detection of test files and functions: Pytest automatically finds files starting with test_ or ending in _test.pyand the functions within them.

  • Powerful fixtures: Fixtures allow you to control the preparation and cleanup of the test environment.

  • Support for asynchronous tests: Pytest works great with asynchronous code through libraries like pytest-asyncio.

  • Flexible parameterization options: Allows you to run tests with different data sets.

Decorator @pytest.mark.parametrize — the heart of parameterization in Pytest. It allows you to run the same test with different sets of input data. The syntax is quite simple.

Decorator syntax:

@pytest.mark.parametrize(argnames, argvalues)
def test_function(argnames):
    # Тело теста
  • argnames: the name or list of parameter names to be used in the test function.

  • argvalues: a list of values ​​or tuples of values ​​corresponding to the parameters.

Example with one parameter:

import pytest

@pytest.mark.parametrize("number", [1, 2, 3, 4, 5])
def test_is_positive(number):
    assert number > 0

Test test_is_positive will be run 5 times with different values number.

Pytest allows you to parameterize tests with multiple arguments, which is good when testing functions with multiple input parameters.

Syntax for multiple arguments:

@pytest.mark.parametrize("arg1, arg2, arg3", [
    (val1_1, val2_1, val3_1),
    (val1_2, val2_2, val3_2),
    # ...
])
def test_function(arg1, arg2, arg3):
    # Тело теста

Example of testing the addition function:

import pytest

@pytest.mark.parametrize("a, b, expected_sum", [
    (1, 2, 3),
    (5, 5, 10),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_addition(a, b, expected_sum):
    assert a + b == expected_sum

Test test_addition will be launched 4 times with different combinations a, b And expected_sum.

You can pass more complex data structures as parameters, such as lists or dictionaries.

Example using dictionaries:

import pytest

test_data = [
    {"input": [1, 2, 3], "expected": 6},
    {"input": [0, 0, 0], "expected": 0},
    {"input": [-1, -2, -3], "expected": -6},
]

@pytest.mark.parametrize("data", test_data)
def test_sum_list(data):
    assert sum(data["input"]) == data["expected"]

Here we pass on the dictionary data into the test function.

It is also possible to combine parameterization with fixtures for more complex testing scenarios.

Example:

import pytest

@pytest.mark.parametrize("username, password", [
    ("user1", "pass1"),
    ("user2", "pass2"),
    ("admin", "adminpass"),
], ids=["User One", "User Two", "Administrator"])
def test_login(username, password):
    # Тест логина с заданными учетными данными
    pass

Here is the fixture base_number provides a base number, and the parameterization provides different values ​​for increment And expected.

To make the test output more readable, you can use the parameter ids in the decorator @pytest.mark.parametrize.

Example:

import pytest

test_values = [1, 2, 3]

@pytest.mark.parametrize("number", test_values)
class TestNumberOperations:

    def test_is_positive(self, number):
        assert number > 0

    def test_is_integer(self, number):
        assert isinstance(number, int)

Now when you run tests, the output will display clear test case IDs.

You can parameterize not only individual functions, but also entire classes or modules with tests.

Class parameterization:

import pytest

@pytest.fixture(params=[("user1", "pass1"), ("user2", "pass2")])
def user_credentials(request):
    return request.param

def test_login(user_credentials):
    username, password = user_credentials
    # Логика теста с использованием username и password
    pass

In this case, each test method inside the class TestNumberOperations will be executed for each value number.

Sometimes you may need to parameterize a fixture. Pytest allows you to do this with a decorator @pytest.fixture with parameter params.

Example:

import pytest

@pytest.fixture
def user_profile(username):
    # Фикстура, которая создает профиль пользователя на основе имени
    return {"username": username, "active": True}

@pytest.mark.parametrize("username", ["alice", "bob"], indirect=True)
def test_user_profile(user_profile):
    assert user_profile["active"] is True

If you want to pass a parameter to a fixture, you can use the parameter indirect.

Example:

import pytest

@pytest.fixture
def user_profile(username):
    # Фикстура, которая создает профиль пользователя на основе имени
    return {"username": username, "active": True}

@pytest.mark.parametrize("username", ["alice", "bob"], indirect=True)
def test_user_profile(user_profile):
    assert user_profile["active"] is True

Parameter username is passed to the fixture user_profile.

Examples of parameterized tests in Pytest

Parameterization of tests with simple data types

Let's start with the most basic example – parameterization using simple data types such as numbers and strings.

import pytest

@pytest.mark.parametrize("input_value, expected_output", [
    (1, True),
    (0, False),
    (-1, False),
    (100, True),
])
def test_is_positive(input_value, expected_output):
    assert (input_value > 0) == expected_output

We check the function that determines whether a number is positive.

Using a decorator @pytest.mark.parametrize we pass a list of tuples, each of which contains input_value And expected_output.

Lists and dictionaries in parameterization

Sometimes input data and expected results are better represented as more complex structures, such as lists or dictionaries.

import pytest

@pytest.mark.parametrize("user_data", [
    {"username": "artem", "password": "wonderland", "status": 200},
    {"username": "ivan", "password": "builder", "status": 200},
    {"username": "nikita", "password": "", "status": 400},
])
def test_user_login(user_data):
    response = simulate_login(user_data["username"], user_data["password"])
    assert response.status_code == user_data["status"]

def simulate_login(username, password):
    # упрощенная функция для имитации логина
    if username and password:
        return MockResponse(200)
    else:
        return MockResponse(400)

class MockResponse:
    def __init__(self, status_code):
        self.status_code = status_code

we test the login function with different sets of data. We pass a dictionary to the test function user_datacontaining the parameters for the test.

Parameterization with fixtures

Fixtures in Pytest are a way to prepare and clean up the test environment. They can be parameterized.

import pytest

@pytest.fixture(params=[
    {"db_name": "test_db_1", "user": "user1"},
    {"db_name": "test_db_2", "user": "user2"},
])
def db_connection(request):
    db = connect_to_database(request.param["db_name"], request.param["user"])
    yield db
    db.disconnect()

def connect_to_database(db_name, user):
    # функция подключения к базе данных
    return MockDatabaseConnection(db_name, user)

class MockDatabaseConnection:
    def __init__(self, db_name, user):
        self.db_name = db_name
        self.user = user
    def disconnect(self):
        pass

def test_database_query(db_connection):
    result = db_connection.query("SELECT * FROM test_table;")
    assert result is not None

We test the execution of a database query with different connection parameters.

Fixture db_connection parameterized and creates a connection to the database with different settings.

Loading test data from external sources

When there is a lot of test data or it changes frequently, it makes sense to store it in external files.

Example from CSV file:

import pytest
import csv

def get_test_data_from_csv():
    with open('test_data.csv', newline="") as csvfile:
        reader = csv.DictReader(csvfile)
        return [row for row in reader]

@pytest.mark.parametrize("username, password, expected_status", get_test_data_from_csv())
def test_external_data(username, password, expected_status):
    response = simulate_login(username, password)
    assert response.status_code == int(expected_status)

# Содержание файла test_data.csv:
# username,password,expected_status
# artem,wonderland,200
# ivan,builder,200
# nikita,hack,403

# повторно используемая функция simulate_login и класс MockResponse из предыдущих примеров
def simulate_login(username, password):
    #  функция для имитации логина
    if username == "nikita":
        return MockResponse(403)
    elif username and password:
        return MockResponse(200)
    else:
        return MockResponse(400)

class MockResponse:
    def __init__(self, status_code):
        self.status_code = status_code

Loading test data from a file test_data.csv and use them to parameterize the test.

Function get_test_data_from_csv reads data from a CSV file and returns a list of tuples for parameterization.

You can learn more about the capabilities of the Pytest library at official documentation.


On September 25, as part of the course “Python Developer. Professional” there will be an open lesson on the topic “Django Class Based Views”. After participating in the lesson, you will be able to easily and quickly create your own class-based views in Django in a few lines of code. If you are interested, sign up using the link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *