
Как проверить несколько входных случаев одним параметризованным тестом pytest


Когда один параметризованный тест лучше набора почти одинаковых проверок
Если вы проверяете чистую функцию, то сама форма теста обычно не меняется: подали вход, получили результат, сравнили с ожидаемым. Меняются только числа. Из-за этого шесть отдельных тестов вроде test_shipping_0, test_shipping_999 и test_shipping_1000 быстро превращаются в копии друг друга. Читать их неудобно: правило размазано по файлу, а повторяющийся код скрывает границы, где чаще всего и живет ошибка.
Параметризация полезна именно в таких задачах. Она позволяет вынести правило в таблицу: вот вход, вот ожидаемый результат, вот одна и та же проверка, которая выполняется для каждого случая. Для функции стоимости доставки это особенно наглядно, потому что логика строится на порогах. Если разработчик случайно напишет > вместо >=, обычные суммы могут пройти, а значения ровно на пороге — нет. Один параметризованный тест собирает эти точки в одном месте и делает ошибку заметной.
Таблица входов и ожидаемых результатов
Начать лучше не с декоратора, а с маленькой таблицы. Она дисциплинирует: вы сначала формулируете правило в числах, а потом уже переносите его в код. Возьмем полностью условную учебную модель, без связи с реальными тарифами: если сумма заказа меньше 1000, доставка стоит 300; если сумма от 1000 до 2999, доставка стоит 150; если сумма от 3000, доставка бесплатна.
Учебный пример. Наша цель — не перебрать все суммы подряд, а выбрать контрольные точки, которые проверяют арифметику на границах. Для этого достаточно начала шкалы, значений прямо перед порогами и самих порогов.
| Сумма заказа | Ожидаемая стоимость доставки | Зачем этот случай нужен |
|---|---|---|
| 0 | 300 | Нижняя граница допустимых значений |
| 999 | 300 | Число прямо перед первым порогом |
| 1000 | 150 | Первый порог, где цена должна измениться |
| 2999 | 150 | Число прямо перед бесплатной доставкой |
| 3000 | 0 | Порог бесплатной доставки |
| 4500 | 0 | Обычное значение внутри верхнего диапазона |
Такая таблица полезнее длинного списка случайных чисел. Она проверяет именно переходы между зонами. Если функция ошибается на 1000 или 3000, вы сразу видите, что сломано правило сравнения, а не вся логика целиком.
Как превратить таблицу в один тест
Теперь таблицу можно перенести в pytest.mark.parametrize. Ниже пример для среды Python 3 и pytest. Функция специально сделана чистой: она зависит только от входного числа и ничего не читает извне. Это упрощает проверку, потому что каждое выполнение теста изолировано по смыслу.
# shipping.py
def shipping_cost(order_total: int) -> int:
if order_total < 0:
raise ValueError("order_total must be non-negative")
if order_total >= 3000:
return 0
if order_total >= 1000:
return 150
return 300
# test_shipping.py
import pytest
from shipping import shipping_cost
@pytest.mark.parametrize(
"order_total, expected",
[
(0, 300),
(999, 300),
(1000, 150),
(2999, 150),
(3000, 0),
(4500, 0),
],
)
def test_shipping_cost(order_total, expected):
assert shipping_cost(order_total) == expected
Практический смысл здесь в том, что вы формулируете одну проверку: shipping_cost(order_total) == expected. Декоратор просто подставляет в нее разные пары значений. Если одна строка таблицы неверна, упадет только соответствующий запуск, а не весь набор целиком без деталей. Для арифметических правил это удобнее, чем писать отдельную функцию теста на каждую цифру.
Обратите внимание на одно решение в примере: отрицательная сумма обрабатывается в самой функции, но не попадает в эту параметризованную таблицу. Причина простая: там уже проверяется другое поведение — выброс исключения, а не числовой результат. Такой случай лучше вынести в отдельный тест с pytest.raises, чтобы одна таблица проверяла один тип ожидания.
Как сделать названия случаев читаемыми
Когда параметров немного, pytest сам сгенерирует имена запусков. Но как только в таблице появляются границы и похожие числа, полезно назвать случаи явно. Тогда в отчете будет видно не просто [1000-150], а смысловое имя вроде [at-1000]. Это ускоряет чтение падения: вам не нужно снова расшифровывать, что именно обозначает строка.
@pytest.mark.parametrize(
"order_total, expected",
[
pytest.param(0, 300, id="zero-order"),
pytest.param(999, 300, id="below-1000"),
pytest.param(1000, 150, id="at-1000"),
pytest.param(2999, 150, id="below-3000"),
pytest.param(3000, 0, id="at-3000"),
pytest.param(4500, 0, id="above-3000"),
],
)
Хорошие имена описывают правило, а не номер строки. case1 и case2 почти бесполезны, потому что при падении все равно придется открывать таблицу и вспоминать контекст. А вот below-3000 и at-3000 сразу показывают, проверяете ли вы внутренность диапазона или сам переход между диапазонами.
На практике такие имена особенно помогают при регрессии. Если после изменения функции падает только at-1000, вы почти сразу понимаете, что сломан знак сравнения на первом пороге. Это не заменяет анализ кода, но сокращает путь от красного теста к гипотезе о причине.
Как проверить границы и не потерять независимость тестов
Самая частая ошибка в табличных тестах — думать, что больше строк автоматически означает лучшее покрытие. Для функции с порогами важнее не количество чисел, а расположение чисел. Набор 1500, 2000 и 2500 почти ничего не скажет о границах, если вы не проверили 999, 1000, 2999 и 3000. Именно поэтому таблица из шести осмысленных точек обычно полезнее длинного случайного списка.
Есть и вторая тонкость: независимость запусков. В документации pytest отдельно сказано, что значения параметров передаются в тест как есть, без копирования. Для нашего примера это безопасно, потому что числа неизменяемы. Но если вы начнете передавать словари, списки или объекты и менять их прямо в тесте, состояние может «протечь» в следующий случай. Значит, для сложных параметров лучше либо не мутировать входные данные, либо создавать новый объект на каждый запуск.
Проверяемый результат этого учебного кейса такой: таблица ловит типичную граничную ошибку. Если испортить функцию и написать оба сравнения через > вместо >=, то случаи 1000 и 3000 упадут, а остальные могут остаться зелеными. В отчете вы увидите смысловые падения именно на порогах, например:
FAILED test_shipping.py::test_shipping_cost[at-1000] - assert 300 == 150
FAILED test_shipping.py::test_shipping_cost[at-3000] - assert 150 == 0
Это и есть главный практический вывод: параметризация полезна не сама по себе, а как способ собрать в одном месте граничные случаи одного правила. Если таблица составлена осмысленно, один короткий тест дает проверяемый сигнал о том, где именно нарушилась арифметика.
Если вам удобнее изучать такой прием по программе, а не по разрозненным заметкам, в карточке каталога комплекта курсов по Python и автоматизации тестов по сохраненному описанию есть блок про автоматизацию тестирования с помощью pytest, где отдельно упомянуты разработка и запуск тестов, параметризация и фикстуры. При выборе формата полезно самому проверить три вещи: есть ли в программе отдельная тема про параметризацию, дают ли самостоятельную практику с таблицами входов и показывают ли, как разбирать упавший тест по названию случая и ожидаемому результату.





















