Тестируем классы и ошибки
До этого момента мы тестировали только функции. Но в реальных проектах вы будете работать с классами и обрабатывать ошибки.
Цель: Научиться тестировать методы классов и проверять, что код правильно выбрасывает исключения.
Вы точно готовы?
Убедитесь, что понимаете:
# Базовый класс
class Calculator:
def add(self, a, b):
return a + b
# Базовое исключение
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / bЕсли классы или исключения для вас новость — изучите основы Python перед продолжением.
Тестирование методов классов
Создайте класс для тестирования
# src/user.py
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
"""Возвращает приветствие"""
return f"Hello, I'm {self.name}!"
def is_adult(self):
"""Проверяет совершеннолетие"""
return self.age >= 18
def celebrate_birthday(self):
"""Увеличивает возраст на 1"""
self.age += 1
return self.ageНапишите тесты
# tests/test_user.py
from src.user import User
def test_user_creation():
"""Тест создания пользователя"""
user = User("Alice", 25)
assert user.name == "Alice"
assert user.age == 25
def test_greet():
"""Тест метода приветствия"""
user = User("Bob", 30)
greeting = user.greet()
assert greeting == "Hello, I'm Bob!"
def test_is_adult_true():
"""Совершеннолетний пользователь"""
user = User("Alice", 25)
assert user.is_adult() == True
def test_is_adult_false():
"""Несовершеннолетний пользователь"""
user = User("Charlie", 16)
assert user.is_adult() == False
def test_celebrate_birthday():
"""Тест дня рождения"""
user = User("Alice", 25)
new_age = user.celebrate_birthday()
assert new_age == 26
assert user.age == 26 # Проверяем что state изменилсяЗапускаем:
pytest tests/test_user.py -vРезультат:
test_user_creation PASSED [ 20%]
test_greet PASSED [ 40%]
test_is_adult_true PASSED [ 60%]
test_is_adult_false PASSED [ 80%]
test_celebrate_birthday PASSED [100%]
============================= 5 passed in 0.02s =============================✅ Все методы класса протестированы!
Тестирование исключений: pytest.raises
Почему нужно тестировать ошибки?
Плохой код не должен молча провалиться — он должен выбросить понятную ошибку.
Пример: Деление на ноль должно выбросить ValueError, а не вернуть странное значение.
Создайте код с валидацией
# src/validator.py
def validate_email(email):
"""Проверяет email"""
if "@" not in email:
raise ValueError("Email must contain @")
if "." not in email:
raise ValueError("Email must contain domain")
return True
def validate_age(age):
"""Проверяет возраст"""
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age too high")
return True
def divide(a, b):
"""Делит a на b"""
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / bТестируем исключения с pytest.raises
# tests/test_validator.py
import pytest
from src.validator import validate_email, validate_age, divide
def test_validate_email_missing_at():
"""Email без @ должен выбросить ошибку"""
with pytest.raises(ValueError):
validate_email("invalid.email.com")
def test_validate_email_missing_domain():
"""Email без домена должен выбросить ошибку"""
with pytest.raises(ValueError):
validate_email("test@")
def test_validate_age_negative():
"""Отрицательный возраст должен выбросить ошибку"""
with pytest.raises(ValueError):
validate_age(-5)
def test_validate_age_too_high():
"""Слишком большой возраст должен выбросить ошибку"""
with pytest.raises(ValueError):
validate_age(200)
def test_divide_by_zero():
"""Деление на ноль должно выбросить ошибку"""
with pytest.raises(ZeroDivisionError):
divide(10, 0)Что делает pytest.raises:
- ✅ Тест ПРОЙДЁТ, если код выбросил ожидаемое исключение
- ❌ Тест ПРОВАЛИТСЯ, если исключение НЕ было выброшено
- ❌ Тест ПРОВАЛИТСЯ, если выброшено ДРУГОЕ исключение
Запускаем:
pytest tests/test_validator.py -vРезультат:
test_validate_email_missing_at PASSED [ 20%]
test_validate_email_missing_domain PASSED [ 40%]
test_validate_age_negative PASSED [ 60%]
test_validate_age_too_high PASSED [ 80%]
test_divide_by_zero PASSED [100%]
============================= 5 passed in 0.01s =============================Проверка сообщения об ошибке
Можно проверить не только ТИП исключения, но и ТЕКСТ ошибки:
def test_validate_email_with_message():
"""Проверяем и тип, и текст ошибки"""
with pytest.raises(ValueError) as exc_info:
validate_email("invalid.com")
# Проверяем текст ошибки
assert str(exc_info.value) == "Email must contain @"
def test_divide_by_zero_message():
"""Проверяем сообщение при делении на ноль"""
with pytest.raises(ZeroDivisionError) as exc_info:
divide(10, 0)
assert "Cannot divide by zero" in str(exc_info.value)Зачем проверять сообщение?
- Пользователи видят это сообщение при ошибке
- Хорошее сообщение помогает понять ЧТО пошло не так
- Тест защищает от случайного изменения текста ошибки
Практический пример: BankAccount
Создайте класс с валидацией
# src/bank_account.py
class BankAccount:
def __init__(self, balance=0):
if balance < 0:
raise ValueError("Initial balance cannot be negative")
self.balance = balance
def deposit(self, amount):
"""Пополнить счёт"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount
return self.balance
def withdraw(self, amount):
"""Снять деньги"""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balanceНапишите полный набор тестов
# tests/test_bank_account.py
import pytest
from src.bank_account import BankAccount
# Тесты счастливого пути
def test_create_account():
"""Создание счёта с балансом"""
account = BankAccount(100)
assert account.balance == 100
def test_deposit():
"""Пополнение счёта"""
account = BankAccount(100)
new_balance = account.deposit(50)
assert new_balance == 150
assert account.balance == 150
def test_withdraw():
"""Снятие денег"""
account = BankAccount(100)
new_balance = account.withdraw(30)
assert new_balance == 70
assert account.balance == 70
# Тесты ошибок
def test_negative_initial_balance():
"""Нельзя создать счёт с отрицательным балансом"""
with pytest.raises(ValueError) as exc_info:
BankAccount(-100)
assert "cannot be negative" in str(exc_info.value)
def test_deposit_negative():
"""Нельзя пополнить на отрицательную сумму"""
account = BankAccount(100)
with pytest.raises(ValueError):
account.deposit(-50)
def test_withdraw_more_than_balance():
"""Нельзя снять больше чем есть на счёте"""
account = BankAccount(100)
with pytest.raises(ValueError) as exc_info:
account.withdraw(200)
assert "Insufficient funds" in str(exc_info.value)
def test_withdraw_negative():
"""Нельзя снять отрицательную сумму"""
account = BankAccount(100)
with pytest.raises(ValueError):
account.withdraw(-50)Запускаем:
pytest tests/test_bank_account.py -vРезультат:
============================= 7 passed in 0.02s =============================Типичные ошибки
Ошибка #1: Забыли использовать pytest.raises
# ❌ ПЛОХО — тест провалится, потому что исключение не поймано
def test_divide_by_zero_bad():
divide(10, 0) # Это выбросит ошибку и тест упадёт!# ✅ ХОРОШО
def test_divide_by_zero_good():
with pytest.raises(ZeroDivisionError):
divide(10, 0)Ошибка #2: Неправильный тип исключения
# ❌ ПЛОХО — ожидаем ValueError, но выбрасывается ZeroDivisionError
def test_wrong_exception():
with pytest.raises(ValueError): # Ожидаем ValueError
divide(10, 0) # Но выбрасывается ZeroDivisionError!
# Тест провалится: "DID NOT RAISE ValueError"Ошибка #3: Код не выбрасывает исключение
# ❌ ПЛОХО — забыли raise в коде
def bad_validate_age(age):
if age < 0:
print("Age cannot be negative") # Только print!
# Забыли raise!
return True
def test_bad_validation():
with pytest.raises(ValueError):
bad_validate_age(-5) # Исключение НЕ выбросится!
# Тест провалится: "DID NOT RAISE ValueError"Что вы изучили
- Тестирование методов классов — создание объектов в тестах
- pytest.raises — проверка исключений
- Проверка error messages — через
exc_info.value - Тестирование edge cases — негативные сценарии (ошибки)
- Счастливый путь VS ошибки — оба важны!
Следующий урок
Отлично! Теперь вы умеете тестировать классы и ошибки. Но как писать чистые тесты, которые легко читать и поддерживать?
Переходите к уроку 3: AAA-паттерн: тесты которые ловят баги
В следующем уроке вы узнаете:
- Arrange-Act-Assert структуру
- Как писать понятные assert messages
- Один тест = одна концепция
- Когда использовать фикстуры вместо inline setup
Устранение неисправностей
Исключение должно выбрасываться внутри блока with pytest.raises с правильным типом
Проверьте:
- В коде есть raise нужного типа (ValueError, ZeroDivisionError и т.д.)
- Тип в pytest.raises совпадает с ожидаемым
- Вызов, который должен упасть, находится внутри блока with pytest.raises
Проверьте импорт, создание объекта и название метода
Проверьте:
- Класс импортирован правильно:
from src.user import User - Экземпляр создан перед обращением к методам
- Имя метода без опечаток:
user.greet(), а неuser.greeet()
Посмотрите реальное сообщение ошибки и сравнивайте с нужной подстрокой
Проверьте:
- Выведите текст:
print(exc_info.value) - Сравнивайте через in:
assert "Email must contain @" in str(exc_info.value)
with pytest.raises(ValueError) as exc_info:
validate_email("test")
print(f"Actual message: {exc_info.value}") # Посмотрите что там
assert "Email must contain @" in str(exc_info.value)