Первые тесты на pytest: нормальные сценарии, граничные случаи и исключения
Содержание курса
Начнём с самого маленького куска pytest: одна функция, один assert, один понятный результат. Пока не выбираем набор сценариев и не трогаем исключения — сначала важно увидеть, что pytest вообще считает тестом.
Минимальная тест-функция pytest и обычный assert
Тест в pytest — это обычная функция Python. Главное, чтобы pytest смог её найти: файл обычно называют в стиле test_prices.py, а функцию — с префиксом test_.
Например, есть простая функция расчёта итоговой цены:
# prices.py
def apply_discount(price: int, percent: int) -> int:
return price - price * percent // 100
Минимальный тест для неё выглядит без специального класса, без наследования и без методов вроде assertEqual:
# test_prices.py
from prices import apply_discount
def test_apply_discount_reduces_price() -> None:
assert apply_discount(1000, 15) == 850
pytest проходит по тестовым файлам, находит функции test_* и запускает их. Если функция закончилась без исключений, тест считается прошедшим. Если внутри упал assert, это обычный AssertionError, и тест считается упавшим.
Важная удобная деталь pytest — он умеет показывать не только факт падения, но и выражение, которое не сошлось. Если в тесте случайно ожидать неверное значение:
def test_apply_discount_reduces_price() -> None:
assert apply_discount(1000, 15) == 800
pytest покажет строку с assert и фактическое значение. По смыслу это ответ на вопрос: «что реально вернула функция и с чем мы это сравнивали?». Поэтому в pytest обычно пишут простой встроенный assert, а не заводят отдельный набор специальных проверочных методов.
Есть граница, о которой лучше помнить сразу. Подробность вывода зависит не только от самого assert, но и от того, где он находится и насколько прозрачно названа проверка. Если вспомогательная функция лежит прямо в тестовом модуле, pytest обычно всё ещё может показать полезную расшифровку упавшего assert. Но если проверку вынесли в обычный модуль приложения или спрятали несколько разных ожиданий за одним слишком общим helper, диагностика становится менее полезной: по вызову в тесте видно, что сломалась вспомогательная проверка, но смысл конкретного ожидания приходится искать глубже.
# хуже читается: название helper не говорит, какое ожидание проверяется
def assert_discount_is_correct() -> None:
assert apply_discount(1000, 15) == 850
def test_apply_discount_reduces_price() -> None:
assert_discount_is_correct()
Для первых тестов лучше держать главное ожидание прямо в test_*-функции:
# проще диагностировать
def test_apply_discount_reduces_price() -> None:
assert apply_discount(1000, 15) == 850
Если helper всё-таки нужен, пусть его имя описывает предмет проверки, а для неочевидного сравнения можно добавить сообщение вторым аргументом assert:
def test_apply_discount_reduces_price() -> None:
actual = apply_discount(1000, 15)
assert actual == 850, "скидка 15% от 1000 должна дать цену 850"
Так тест остаётся коротким, а причина падения находится рядом с названием проверяемого поведения. На этом уровне нам достаточно запомнить три вещи: файл должен быть похож на тестовый, функция должна начинаться с test_, а проверка результата пишется обычным assert.
