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

Первые тесты на pytest: нормальные сценарии, граничные случаи и исключения

Содержание курса

Проверка ожидаемых исключений через pytest.raises

Иногда правильное поведение функции — не вернуть значение, а отказаться выполнять операцию. Например, банковский счёт не должен уходить в минус, если бизнес-правило это запрещает.

Для таких случаев в pytest используют pytest.raises. Это контекстный менеджер: код, который должен упасть, помещают внутрь блока with, а рядом указывают ожидаемый тип исключения.

# account.py

class InsufficientFundsError(Exception):
    pass

class Account:
    def __init__(self, balance: int) -> None:
        self.balance = balance

    def withdraw(self, amount: int) -> None:
        if amount > self.balance:
            raise InsufficientFundsError("недостаточно средств")
        self.balance -= amount

Тест фиксирует не итоговый баланс, а сам факт отказа:

# test_account.py

import pytest

from account import Account, InsufficientFundsError

def test_withdraw_more_than_balance_raises_error() -> None:
    account = Account(balance=500)

    with pytest.raises(InsufficientFundsError):
        account.withdraw(1000)

Смысл проверки такой: если withdraw(1000) поднял InsufficientFundsError, тест проходит. Если исключения не было, тест падает, потому что ожидался отказ. Если поднялось исключение другого типа, pytest его не считает подходящим и тоже роняет тест.

Иногда одного типа мало. Сообщение исключения тоже может быть частью поведения: по нему логируют ошибку, показывают текст пользователю или отличают одну причину отказа от другой. Тогда результат pytest.raises сохраняют в переменную через as excinfo.

def test_withdraw_more_than_balance_explains_reason() -> None:
    account = Account(balance=500)

    with pytest.raises(InsufficientFundsError) as excinfo:
        account.withdraw(1000)

    assert "недостаточно" in str(excinfo.value)

excinfo.value — это сам объект исключения. Его можно привести к строке и проверить важную часть сообщения. Обычно лучше не сравнивать весь текст целиком, если формулировка может чуть измениться. Проверка ключевого фрагмента устойчивее.

Важная граница: pytest.raises не ловит «любую ошибку примерно рядом». Тип должен совпасть с тем, что вы действительно ожидаете.

def test_wrong_expected_exception_type() -> None:
    account = Account(balance=500)

    with pytest.raises(ValueError):
        account.withdraw(1000)

Этот тест не пройдёт, потому что код поднимает InsufficientFundsError, а тест ждёт ValueError. Это полезно: тест не маскирует неожиданные поломки и заставляет явно назвать контракт функции. Если функция должна сообщать о нехватке денег своим исключением предметной области, тест должен ждать именно его.