Тестирование с pytest

Первые тесты на 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.