Тестирование методов классов, pytest.raises для исключений, проверка текста ошибки
До этого момента мы тестировали только функции. Но в реальных проектах вы будете работать с классами и обрабатывать ошибки.
Цель: научиться тестировать методы классов и проверять, что код правильно выбрасывает исключения.
# 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 =============================
✅ Все методы класса протестированы!
Плохой код не должен молча провалиться — он должен выбросить понятную ошибку.
Пример: деление на ноль должно выбросить 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# 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:
Можно проверить не только ТИП исключения, но и ТЕКСТ ошибки:
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)Зачем проверять сообщение?
# 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)Запускаем:
pytest tests/test_bank_account.py -v============================= 7 passed in 0.02s =============================
# ❌ ПЛОХО — тест провалится, потому что исключение не поймано
def test_divide_by_zero_bad():
divide(10, 0) # Это выбросит ошибку и тест упадёт!
# ✅ ХОРОШО
def test_divide_by_zero_good():
with pytest.raises(ZeroDivisionError):
divide(10, 0)# ❌ ПЛОХО — ожидаем ValueError, но выбрасывается ZeroDivisionError
def test_wrong_exception():
with pytest.raises(ValueError): # Ожидаем ValueError
divide(10, 0) # Но выбрасывается ZeroDivisionError!
# Тест провалится: "DID NOT RAISE ValueError"# ❌ ПЛОХО — забыли 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 — проверка исключенийexc_info.valueВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: AAA-паттерн: тесты, которые ловят баги