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.py
and 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_data
containing 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.