pytest запускает обычные функции с именем test_ и показывает значения, из-за которых проверка не прошла. На функции скидки увидим зелёный запуск, красный diff, параметризацию и pytest.raises. Затем специально сломаем формулу, чтобы тест поймал ошибку до ручной проверки приложения.
Без теста новичок открывает программу, вводит несколько чисел и надеется, что не пропустил границу. Один pytest-файл превращает ожидания в повторяемые примеры. В этой статье вы соберёте набор тестов для функции final_price. Финальный результат можно проверить без догадок: команда pytest -q печатает 6 passed, а неверная формула указывает конкретный набор price и discount.
Готовим цену, скидку и ожидаемый ответ без лишней инфраструктуры.
Вызываем final_price так же, как её вызывает программа.
Сравниваем фактический результат и отдельно проверяем неверные аргументы.
Что именно проверяет pytest
Логика лежит в price.py, а примеры поведения в test_price.py. pytest находит файл по префиксу test_, импортирует функцию и запускает каждую проверку отдельно. Общий результат зависит только от возвращаемого значения, поэтому тест не требует браузера или базы. Для pytest держите этапы раздельно: ввод, проверка, основное действие и вывод. Такой маршрут позволяет остановиться в конкретной точке, увидеть фактические данные и исправить одну причину вместо случайной перестановки строк.
| Этап | Действие | Проверяемый результат |
|---|---|---|
| Сбор | pytest ищет test_*.py | Найдены функции test_* |
| Подготовка | Параметры создают несколько случаев | У каждого случая свои значения |
| Проверка | Обычный assert сравнивает результат | pytest строит читаемый diff |
| Отчёт | Считаются passed и failed | Команда возвращает код завершения |
Один красный случай содержит вход, ожидание и фактическое значение, поэтому его можно повторить отдельно. У pytest каждый этап оставляет наблюдаемый след, поэтому следующий шаг начинается только после проверки предыдущего.

От входных данных до видимого результата. Каждый сценарий получает собственный результат
Пишем функцию скидки и шесть тестов
Создайте проект uv init discount-tests, добавьте uv add --dev pytest и сохраните два файла из примера. Запуск uv run pytest -q использует окружение проекта. Флаг -q сокращает отчёт, а -vv показывает параметры каждого случая. Не называйте собственный файл pytest.py: он перекроет установленный пакет.
# price.py
def final_price(price: float, discount: int) -> float:
if price < 0:
raise ValueError("price must be non-negative")
if not 0 <= discount <= 100:
raise ValueError("discount must be from 0 to 100")
return round(price * (1 - discount / 100), 2)
# test_price.py
import pytest
from price import final_price
@pytest.mark.parametrize(
("price", "discount", "expected"),
[(100, 0, 100), (100, 25, 75), (99.90, 10, 89.91)],
)
def test_final_price(price, discount, expected):
assert final_price(price, discount) == expected
def test_rejects_negative_price():
with pytest.raises(ValueError, match="non-negative"):
final_price(-1, 10)- Три набора parametrization запускаются как отдельные тесты.
- Нулевая скидка оставляет цену без изменений.
- Десять процентов от 99.90 дают 89.91.
- Отрицательная цена поднимает ожидаемый ValueError.
Функция остаётся чистой: она не читает input и не печатает ответ. Поэтому тест подаёт аргументы напрямую и не зависит от терминала. Пример pytest специально ограничен одним пользовательским сценарием, чтобы назначение каждой строки оставалось видимым.

Что отличает устойчивый проект от случайного запуска. Красный тест показывает вход и ожидание
Как читать падение assert и параметризацию
pytest переписывает выражение assert при импорте тестового модуля. Благодаря этому отчёт показывает не просто False, а сравниваемые значения. mark.parametrize создаёт отдельный вызов для каждой строки таблицы. Контекст pytest.raises считает тест успешным только при нужном классе исключения, а match дополнительно проверяет сообщение. Код завершения 0 означает, что набор прошёл, и именно его использует CI. Фикстуры здесь не нужны: сначала полезно научиться тестировать чистые функции без скрытой подготовки.
| Часть | За что отвечает | Что проверить |
|---|---|---|
| Функция | Возвращает результат без ввода | Одинаковый вход даёт одинаковый ответ |
| test_ файл | Хранит примеры поведения | Импорт указывает на ваш модуль |
| assert | Фиксирует ожидаемое значение | Сравнение читается в отчёте |
| parametrize | Размножает один сценарий | Каждая граница отдельной строкой |
| raises | Проверяет отказ функции | Класс и текст исключения точны |
Тест должен падать по правильной причине. Если вы ожидаете ValueError, но функция падает с TypeError раньше, слишком широкий raises(Exception) скроет дефект. Числа с плавающей точкой иногда требуют pytest.approx, однако денежный код лучше строить на Decimal. Не гонитесь за процентом покрытия: строка может выполняться и при этом не проверяться. Сильный тест называет наблюдаемое правило и содержит границу, например скидку 0, 100 и значение за диапазоном.
Ломаем формулу и усиливаем набор тестов
Добавьте случаи discount=100 и discount=101. Второй должен проверяться через pytest.raises. Затем поменяйте round(..., 2) на round(..., 0) и прочитайте diff. Создайте test_identity_without_discount, чтобы имя теста объясняло правило. После исправления запустите один тест по узлу pytest test_price.py::test_rejects_negative_price -q, а затем весь набор. В конце добавьте функцию calculate_cart_total и проверьте пустой список.
- Получите зелёный запуск исходного набора.
- Сломайте деление и зафиксируйте красный diff.
- Добавьте границу скидки 100 процентов.
- Проверьте отказ для скидки 101.
- Запустите один тест по node id.
- Верните код и получите полный зелёный отчёт.
- Все примеры проходят повторно.
- Сломанная формула ловится.
- Границы описаны явно.
- Исключения проверяются точно.
Набор ценен не числом функций test_, а тем, что он ловит реальное изменение правила. После этой проверки измените одно правило pytest и заранее запишите ожидаемый результат. Так копирование превращается в понимание.

Четыре проверки перед следующим шагом. Границы важнее большого числа строк
Как не превратить тесты в вторую программу
Дальше вынесите тесты в каталог tests и запускайте их перед каждым push. Фикстуры добавляйте, когда одинаковая подготовка появляется минимум в нескольких тестах. Для внешнего API сначала подменяйте сеть, иначе падение интернета будет выглядеть как дефект функции. Медленные интеграционные сценарии держите отдельно от быстрых unit-тестов. И не исправляйте тест только потому, что он красный: сначала решите, изменилось требование или код.
Как изучить тему в Кодике
Закрепите функции, возврат значений и исключения Python, затем превратите каждый пример в отдельное правило. В Кодике сначала восстановите чистую логику без фреймворка, затем верните библиотеку и интерфейс pytest.
| Шаг | Что разобрать | Мини-проверка |
|---|---|---|
| 1 | Чистые функции | Убрать input и print из расчёта |
| 2 | assert | Сравнить два значения |
| 3 | Списки случаев | Выделить три границы |
| 4 | Исключения | Поймать ValueError |
| 5 | Автоматический запуск | Связать тесты с push |
Сначала проверяйте маленькую функцию за миллисекунды, только потом подключайте файлы, сеть и UI. Финальная проверка для pytest: закрыть статью, объяснить путь данных и добавить одну функцию без копирования готового блока.
Переименуйте его, иначе import pytest загрузит локальный файл.
Разделите сценарии, чтобы падение указывало на конкретное поведение.
Проверяйте точный класс и при необходимости match сообщения.
Сравнивайте с известным ответом, а не вычисляйте expected тем же алгоритмом.
Что получится в итоге

Один запуск показывает шесть независимых примеров, а намеренная ошибка формулы оставляет точный diff с входными параметрами.
Как pytest находит тесты?
pytest ищет файлы и функции с именами по шаблону test_ и запускает их независимо.
Зачем нужен pytest.mark.parametrize?
parametrize прогоняет одно правило на нескольких наборах данных и подписывает каждый случай.
Как проверить исключение в pytest?
Используйте pytest.raises с точным классом исключения и при необходимости параметром match.
Как понять, что учебный проект pytest действительно работает?
Проверьте основной сценарий pytest, неверный ввод, повторный запуск и один граничный случай. Затем объясните путь данных своими словами.
Можно ли начать с готового кода pytest из статьи?
Да. Сначала добейтесь результата pytest, затем измените одно правило и восстановите ключевой файл без копирования.
Функции и исключения закрепите в курсе Python. Затем добавьте этот набор в GitHub Actions для Python.
Для браузерного уровня посмотрите сценарий Playwright, а для API сравните тестирование FastAPI.