Skip to main content
Back to course
Pytest с нуля: тесты, которые реально работают
3 / 1127%

🧪 Тестируем классы и ошибки: pytest.raises и первая фикстура

20 минут

⚠️ Урок устарел. Свежая версия в курсе 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, чтобы тест подсветил проблему

Задание:

  1. Напишите фикстуру empty_manager(), возвращающую новый TaskManager.
  2. Тесты:
  • 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 в тестах. 🎯

🧪 Тестируем классы и ошибки: pytest.raises и первая фикстура — Pytest с нуля: тесты, которые реально работают — Potapov.me