Первые тесты на 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. Это полезно: тест не маскирует неожиданные поломки и заставляет явно назвать контракт функции. Если функция должна сообщать о нехватке денег своим исключением предметной области, тест должен ждать именно его.
