🐛 Тесты, которые ловят настоящие баги: AAA и assert
⚠️ Урок устарел. Свежая версия в курсе pytest-basics: AAA-паттерн: тесты которые ловят баги.
Научитесь находить баги ДО пользователей
❌ Сейчас: «Мой код работал, а в production сломался!»
✅ После урока: «Я знаю, какие тесты писать, чтобы баги не доходили до пользователей»
Увидите разницу между «тест проходит» и «тест полезен»
Научитесь думать как «ломатель»: граничные случаи и неверные данные
Поймёте AAA как инструмент мышления, а не просто форматирование
Демонстрация: тест, который поймал бы production-баг
# ❌ Баг: BankAccount позволяет уйти в минус
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
self.balance -= amount # нет проверки на лимит
# ✅ Исправленный код
class BankAccountFixed:
def __init__(self, balance=0, max_balance=10_000):
self.balance = balance
self.max_balance = max_balance
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Not enough money")
self.balance -= amount
def deposit(self, amount):
if self.balance + amount > self.max_balance:
raise ValueError(f"Balance cannot exceed {self.max_balance}")
self.balance += amountimport pytest
def test_cannot_withdraw_to_negative_balance():
account = BankAccount()
account.balance = 100
with pytest.raises(ValueError, match="Not enough money"):
account.withdraw(200) # 100 - 200 = -100 🚨
assert account.balance == 100 # баланс не изменился
def test_deposit_respects_max_balance():
account = BankAccountFixed(balance=9_500, max_balance=10_000)
with pytest.raises(ValueError, match="cannot exceed"):
account.deposit(600) # 9_500 + 600 = 10_100 > 10_000AAA: не про выравнивание, а про мышление
- Arrange — подготовить всё: данные, окружение, моки. После этого шага тестируемая функция готова к вызову.
- Act — одно действие, один вызов. Если действий два, это уже два теста.
- Assert — проверка результата: возврат, изменённое состояние или побочный эффект.
def test_withdraw_reduces_balance():
# ARRANGE — готовим данные
account = BankAccountFixed(balance=200)
amount = 50
# ACT — одно действие
account.withdraw(amount)
# ASSERT — проверяем результат
assert account.balance == 150Один тест — одно действие
# ❌ Два действия — непонятно, что сломалось
def test_add_and_complete_task():
manager = TaskManager()
manager.add("task 1")
manager.complete(0)
assert manager.all() == []
# ✅ Разносим
def test_add_task():
manager = TaskManager()
manager.add("task 1")
assert manager.all() == ["task 1"]
def test_complete_task_by_index():
manager = TaskManager()
manager.add("task 1")
manager.complete(0)
assert manager.all() == []Думайте как ломатель: 5 типов граничных случаев
- Пустые значения:
None,"",[] - Крайние числа:
0, отрицательные, «слишком большие» - Неправильные типы: строка вместо числа
- Повторения: два одинаковых вызова подряд
- Состояние системы: пустой баланс, заполненный список
Примеры для BankAccount:
def test_deposit_negative_amount():
account = BankAccountFixed()
with pytest.raises(ValueError):
account.deposit(-100)
def test_deposit_zero_amount():
account = BankAccountFixed()
with pytest.raises(ValueError):
account.deposit(0)
def test_withdraw_from_empty_account():
account = BankAccountFixed()
with pytest.raises(ValueError, match="Not enough money"):
account.withdraw(100)Минимальные assert-паттерны (ничего лишнего)
import pytest
from src.task_manager import TaskManager
def test_assert_patterns():
manager = TaskManager()
manager.add("task 1")
# ✅ Проверка наличия/отсутствия в коллекции
assert "task 1" in manager.all()
assert "task 2" not in manager.all()
# ✅ Исключение с текстом
with pytest.raises(ValueError, match="empty"):
manager.add("")
# ✅ Тип — когда критично для логики
assert isinstance(manager.all(), list)Анти‑паттерн: «мега-тест»
# ❌ Слишком много ответственности
def test_task_manager_does_everything():
manager = TaskManager()
manager.add("task 1")
manager.add("task 2")
manager.complete(0)
manager.complete(1)
with pytest.raises(ValueError):
manager.add("") # валидация + завершение + дубли — всё вместе
# ✅ Разбиваем
def test_task_manager_adds_task():
manager = TaskManager()
manager.add("task 1")
assert manager.all() == ["task 1"]
def test_task_manager_validates_empty_text():
manager = TaskManager()
with pytest.raises(ValueError):
manager.add("")
def test_task_manager_completes_by_index():
manager = TaskManager()
manager.add("task 1")
manager.complete(0)
assert manager.all() == []Уровни сложности
🎯 Базовый: один тест — одно действие, AAA, базовые assert'ы.
🚀 Продвинутый: 5 типов граничных случаев, шпаргалка вопросов, анти-паттерны и их разбиение.
Практика
Найдите баги в TaskManager:
class TaskManager:
def __init__(self):
self.tasks = []
def add_task(self, text):
self.tasks.append(text) # 🐛 валидация отсутствует
def complete_task(self, index):
del self.tasks[index] # 🐛 нет проверки границНапишите 3 теста, которые словят баги:
def test_add_task_empty_text_raises_error():
"""Пустая задача должна вызывать ValueError"""
manager = TaskManager()
with pytest.raises(ValueError):
manager.add_task("")
def test_complete_task_invalid_index_raises_error():
"""Несуществующий индекс должен вызывать IndexError"""
manager = TaskManager()
manager.add_task("task 1")
with pytest.raises(IndexError):
manager.complete_task(999)
def test_add_task_long_text_works():
"""Длинный текст (1000+ символов) сохраняется полностью"""
manager = TaskManager()
long_text = "x" * 1500
manager.add_task(long_text)
assert manager.tasks[0] == long_textШпаргалка: 5 вопросов для каждого теста
Держите под рукой, когда пишете тесты.
📝 Про входные данные
- Что если передать
None? - Пустая строка/список?
- Очень длинные данные?
- Спецсимволы?
- Неправильный тип?
🔢 Про числа
- Ноль?
- Отрицательные?
- Очень большие?
- Дробные?
🕒 Про состояние
- Функция вызывается дважды?
- Данные уже существуют?
- «Пограничное» состояние системы?
Чеклист: проверьте тест перед коммитом
- Одно поведение — тест проверяет конкретную функциональность
- Понятное имя —
test_что_проверяем_при_каких_условиях - Полный Arrange — все данные подготовлены до вызова функции
- Чистый Act — одна строка вызова тестируемой функции
- Точный Assert — проверяет результат, а не процесс
- Граничные случаи — есть проверки краёв и ошибок
- Ясность при падении — по сообщению понятно, что сломалось
Бонус: покажите тест коллеге — если понял за 10 секунд, тест хороший.
Теперь вы не просто запускаете тесты — вы проверяете качество кода до того, как баги увидят пользователей. 🎯