🧪 Тестируем классы и ошибки: pytest.raises и первая фикстура
⚠️ Урок устарел. Свежая версия в курсе pytest-basics: Тестируем классы и ошибки.
Зачем этот урок
✅ Пишем тесты для классов, а не только для функций
✅ Ловим ошибки правильно (pytest.raises вместо try/except)
✅ Показываем первую фикстуру, чтобы не копировать создание объекта
Стартовый код
src/bank_account.py:
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise ValueError("Not enough money")
self.balance -= amountСтруктура проекта:
bank-app/
src/bank_account.py
tests/test_bank_account.pyЗапуск: PYTHONPATH=. pytest -v
Базовые тесты класса
tests/test_bank_account.py:
from src.bank_account import BankAccount
def test_deposit_increases_balance():
account = BankAccount()
account.deposit(200)
assert account.balance == 200
def test_withdraw_reduces_balance():
account = BankAccount()
account.deposit(150)
account.withdraw(50)
assert account.balance == 100Проверяем ошибки через pytest.raises
import pytest
from src.bank_account import BankAccount
def test_cannot_withdraw_more_than_balance():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Not enough money"):
account.withdraw(150)
# ✅ Тест пройдёт только если исключение поднято
# ✅ match страхует от «другого» ValueError
def test_amount_must_be_positive_on_deposit():
account = BankAccount()
with pytest.raises(ValueError, match="positive"):
account.deposit(0)Почему так лучше, чем try/except: тест падает, если исключение не поднято, и сообщение контролируемо (match), без ручного assert False.
Убираем дублирование первой фикстурой
Проблема до фикстуры: одно и то же создание объекта в каждом тесте.
# ДО: дублирование
def test_deposit_increases_balance():
account = BankAccount() # повторяется
account.deposit(200)
assert account.balance == 200
def test_withdraw_reduces_balance():
account = BankAccount() # повторяется
account.deposit(150)
account.withdraw(50)
assert account.balance == 100Решение: вынести создание в фикстуру.
import pytest
from src.bank_account import BankAccount
@pytest.fixture
def account():
return BankAccount()
def test_deposit_increases_balance(account):
account.deposit(200)
assert account.balance == 200
def test_cannot_withdraw_more_than_balance(account):
account.deposit(100)
with pytest.raises(ValueError, match="Not enough money"):
account.withdraw(150)На этом этапе фикстура — всего лишь функция, которая создаёт объект. В следующем уроке углубимся в скоупы и cleanup.
Практика: TaskManager с ошибками
src/task_manager.py:
class TaskManager:
def __init__(self):
self.tasks = []
def add(self, text: str):
cleaned = text.strip()
if not cleaned:
raise ValueError("Task cannot be empty")
self.tasks.append(cleaned)
return cleaned
def all(self) -> list[str]:
return self.tasks # специально без copy, чтобы тест подсветил проблемуЗадание:
- Напишите фикстуру
empty_manager(), возвращающую новыйTaskManager. - Тесты:
test_add_trims_whitespace— пробелы обрезаются.test_empty_task_raises_error— проверка черезpytest.raisesиmatch="empty".test_all_returns_copy— после внешней модификации возвращённого спискаall()должен вернуть исходные данные. Код сейчас без copy — тест покажет баг.
Чеклист
- Один класс — несколько коротких тестов на разные ветки.
- Ошибки проверяются через
pytest.raises, при необходимости сmatch. - Повторяющийся setup вынесен в простую фикстуру.
- Запуск
PYTHONPATH=. pytest -vзелёный.
Дальше — полноценные фикстуры, параметризация и маркеры. А пока убедитесь, что ошибки покрыты, а код читается без try/except в тестах. 🎯