@pytest.mark.parametrize, ids для читаемых названий, множественная параметризация
Представьте: нужно протестировать функцию validate_email() с 10 разными email'ами. Копировать тест 10 раз?
Цель: научиться использовать @pytest.mark.parametrize для тестирования множества сценариев одним тестом.
Плохой подход: 5 одинаковых тестов
# ❌ ПЛОХО — повторяем один и тот же тест
def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_zeros():
assert add(0, 0) == 0
def test_add_negative_numbers():
assert add(-1, -2) == -3
def test_add_mixed_numbers():
assert add(-1, 5) == 4
def test_add_large_numbers():
assert add(1000, 2000) == 3000Проблемы:
# ✅ ХОРОШО — один тест, 5 сценариев
import pytest
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(0, 0, 0),
(-1, -2, -3),
(-1, 5, 4),
(1000, 2000, 3000),
])
def test_add(a, b, expected):
"""Тест сложения с разными числами"""
result = add(a, b)
assert result == expectedЧто делает pytest:
[(2, 3, 5), (0, 0, 0), ...]Запускаем:
pytest test_calculator.py::test_add -vРезультат:
test_add[2-3-5] PASSED [ 20%]
test_add[0-0-0] PASSED [ 40%]
test_add[-1--2--3] PASSED [ 60%]
test_add[-1-5-4] PASSED [ 80%]
test_add[1000-2000-3000] PASSED [100%]
============================= 5 passed in 0.01s =============================
✅ Один тест превратился в 5 отдельных!
@pytest.mark.parametrize("параметры", [
(значение1_для_параметра1, значение1_для_параметра2, ...),
(значение2_для_параметра1, значение2_для_параметра2, ...),
])
def test_функция(параметр1, параметр2, ...):
passПример с одним параметром:
@pytest.mark.parametrize("number", [1, 2, 3, 4, 5])
def test_is_positive(number):
"""Все числа положительные"""
assert number > 0Пример с тремя параметрами:
@pytest.mark.parametrize("username, password, expected_success", [
("admin", "secret123", True),
("user", "wrongpass", False),
("", "nouser", False),
])
def test_login(username, password, expected_success):
result = login(username, password)
assert result.success == expected_successПо умолчанию pytest генерирует названия из значений:
test_validate_email[test@example.com] PASSED
test_validate_email[invalid.com] FAILEDДля простых значений — ОК. Но для сложных объектов:
test_user[<User object at 0x7f...>] PASSED # Непонятно!import pytest
@pytest.mark.parametrize("email, is_valid", [
("test@example.com", True),
("user.name@domain.co.uk", True),
("invalid.com", False),
("@nodomain", False),
("no-at-sign.com", False),
], ids=[
"valid-simple",
"valid-subdomain",
"missing-at",
"missing-username",
"missing-domain",
])
def test_validate_email(email, is_valid):
"""Валидация email'ов"""
result = validate_email(email)
assert result == is_validРезультат:
test_validate_email[valid-simple] PASSED [ 20%]
test_validate_email[valid-subdomain] PASSED [ 40%]
test_validate_email[missing-at] FAILED [ 60%]✅ Теперь понятно какой тест-кейс упал!
def idfn(test_input):
"""Генерирует id из входных данных"""
if test_input.startswith("valid"):
return f"valid-case-{test_input[:10]}"
return f"invalid-case-{test_input[:10]}"
@pytest.mark.parametrize("email", [
"valid@example.com",
"invalid.com",
], ids=idfn)
def test_email(email):
passimport pytest
from src.bank_account import BankAccount
@pytest.mark.parametrize("initial, deposit, expected", [
(100, 50, 150),
(0, 100, 100),
(1000, 1, 1001),
(50, 50, 100),
], ids=[
"normal-deposit",
"deposit-to-empty",
"small-deposit-to-large",
"equal-amounts",
])
def test_deposit_scenarios(initial, deposit, expected):
"""Различные сценарии пополнения"""
# Arrange
account = BankAccount(initial)
# Act
new_balance = account.deposit(deposit)
# Assert
assert new_balance == expected, (
f"Deposit {deposit} to {initial} should give {expected}, "
f"got {new_balance}"
)@pytest.mark.parametrize("initial, amount, error_msg", [
(100, -50, "must be positive"),
(100, 0, "must be positive"),
], ids=[
"negative-deposit",
"zero-deposit",
])
def test_invalid_deposit(initial, amount, error_msg):
"""Невалидные пополнения должны выбрасывать ошибку"""
account = BankAccount(initial)
with pytest.raises(ValueError) as exc_info:
account.deposit(amount)
assert error_msg in str(exc_info.value)Можно применить @pytest.mark.parametrize ДВАЖДЫ:
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_combinations(x, y):
"""Тестирует все комбинации x и y"""
assert x * y >= 0Результат: 4 теста (2 значения x × 2 значения y)
test_combinations[2-0] PASSED # x=0, y=2
test_combinations[2-1] PASSED # x=1, y=2
test_combinations[3-0] PASSED # x=0, y=3
test_combinations[3-1] PASSED # x=1, y=3
# ❌ ПЛОХО — тесты проверяют РАЗНЫЕ концепции
@pytest.mark.parametrize("operation", [
"deposit",
"withdraw",
"get_balance",
])
def test_bank_operations(operation):
# Слишком разная логика для каждого случая
pass# ❌ ПЛОХО — только 1-2 сценария
@pytest.mark.parametrize("value", [5]) # Зачем parametrize?
def test_single_value(value):
assert value == 5Когда кейсов много, удобнее хранить их в словаре и явно вытаскивать поля:
import pytest
test_cases = {
"valid_simple": {
"email": "test@example.com",
"is_valid": True,
},
"invalid_no_at": {
"email": "invalid.com",
"is_valid": False,
},
}
@pytest.mark.parametrize(
"email, is_valid",
[(case["email"], case["is_valid"]) for case in test_cases.values()],
ids=list(test_cases.keys()),
)
def test_validate_email(email, is_valid):
result = validate_email(email)
assert result == is_valid@pytest.mark.parametrize — один тест, много сценариевids parameter — понятные названия тестовВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Маркеры: запускаем нужные тесты