Параметризация pytest: один тест, несколько сценариев

Параметризация pytest: один тест, несколько сценариев
Содержание статьи

pytest.mark.parametrize запускает один тест несколько раз с разными аргументами. Это удобно, когда правило проверки одинаковое, а входные данные должны покрыть разные границы. Каждый набор становится отдельным тестовым случаем с собственным результатом.

В примере Mentoring.digital проверим число страниц в выдаче. Задача небольшая, поэтому видно, что именно защищает каждый случай и почему одного «обычного» значения недостаточно.

Опишите правило до списка параметров

Пусть функция получает два целых числа: количество элементов total и положительный размер страницы page_size. Для пустой выдачи нужно вернуть 0, для неполной страницы округлить число страниц вверх. Отрицательное количество элементов и неположительный размер страницы считаем ошибкой. Проверка типов в этот учебный контракт не входит.

Создайте файл pagination.py:

def page_count(total, page_size):
    if total < 0 or page_size <= 0:
        raise ValueError("invalid pagination")
    return (total + page_size - 1) // page_size

Полезно заранее выписать классы входов: пустая выдача, один элемент, полная страница, первый элемент следующей страницы и несколько полных страниц. Здесь важна разница поведения, а не количество случайных чисел.

Передайте входы и ожидаемый результат

Установите pytest в отдельное виртуальное окружение проекта. В командах ниже python должен указывать на его интерпретатор:

python -m pip install pytest
python -m pytest --version

Рядом с модулем создайте test_pagination.py. Первый аргумент декоратора перечисляет имена параметров. Каждая строка данных содержит значения в том же порядке:

import pytest
from pagination import page_count

@pytest.mark.parametrize(
    "total,size,expected",
    [
        pytest.param(0, 20, 0, id="empty"),
        pytest.param(1, 20, 1, id="one-item"),
        pytest.param(20, 20, 1, id="exact-page"),
        pytest.param(21, 20, 2, id="partial-page"),
        pytest.param(40, 20, 2, id="two-pages"),
    ],
)
def test_page_count(total, size, expected):
    assert page_count(total, size) == expected

У всех пяти случаев одна проверка: фактическое число страниц равно ожидаемому. Добавление данных не меняет тело теста. Если для нового сценария приходится писать условие внутри теста и выбирать совсем другой способ проверки, возможно, ему нужна отдельная функция.

Сделайте ошибки понятными через IDs

Имена empty, exact-page и partial-page описывают назначение случаев. По имени упавшего теста будет понятно, какая граница нарушена. Идентификатор не влияет на логику проверки.

python -m pytest --collect-only -q
python -m pytest -q -k partial-page

Первая команда показывает собранные случаи, вторая выбирает тесты, чьё имя соответствует выражению -k. Перед отладкой большой матрицы стоит посмотреть сборку: случайно созданные комбинации заметны ещё до запуска.

Не складывайте в ID токены доступа, персональные данные и большие исходные документы. Имена тестов попадают в логи. Короткое название сценария полезнее полного представления сложного объекта.

Проверьте исключения отдельным тестом

Неверные входы относятся к другому ожидаемому поведению. Добавьте в тот же тестовый файл ещё одну параметризованную функцию:

@pytest.mark.parametrize(
    "total,size", [(-1, 20), (1, 0), (1, -5)],
    ids=["negative-total", "zero-size", "negative-size"],
)
def test_invalid_pagination(total, size):
    with pytest.raises(ValueError, match="invalid"):
        page_count(total, size)

pytest.raises требует указанное исключение внутри своего блока. Выражение match проверяет текст ошибки как регулярное выражение. Если ошибка не возникла или имеет другой тип, случай не пройдёт.

Не объединяйте успешные и ошибочные сценарии только ради одной функции. Отдельные тесты здесь проще читать: один подтверждает результат, другой подтверждает отказ от недопустимого входа.

Убедитесь, что тесты ловят дефект

python -m pytest -q

В локальном окружении Python 3.12.10 и pytest 9.1.1 этот набор дал восемь успешных случаев: пять расчётов и три исключения. Затем мы намеренно заменили формулу на total // page_size. Упали случаи one-item и partial-page; остальные шесть прошли. После восстановления формулы снова прошли все восемь.

Это полезная проверка смысла теста. Зелёный запуск показывает согласованность текущего кода и ожиданий, а контролируемый дефект показывает, обнаруживает ли набор интересующую ошибку. Такой эксперимент делайте только в своей учебной копии или рабочей ветке с понятным восстановлением.

Не превращайте параметры в скрытое состояние

  • pytest передаёт объекты параметров как есть. Если два случая используют один изменяемый список или словарь и тест меняет его, следующий случай может получить уже другие данные.
  • Несколько декораторов parametrize образуют комбинации параметров. Прежде чем увеличивать матрицу, проверьте, нужны ли все сочетания.
  • Подготовку базы данных или другого дорогого ресурса удобнее вынести в fixture. Для передачи параметра в fixture существует indirect, но в этом простом расчёте он не нужен.
  • Ожидаемый результат должен следовать из правила. Копия формулы реализации в assert может повторить ту же ошибку.

Если трудно выбрать границы, на занятии с ментором по автотестированию можно разобрать один реальный модуль и составить набор проверок по его контракту. А при затруднениях с исключениями и устройством функций поможет наставник по Python.

Похожие статьи по данной теме