Учимся думать как тестировщик: пишем тесты, которые находят реальные баги до продакшена через AAA, граничные случаи и понятные assert
⚠️ Урок устарел. Свежая версия в курсе pytest-basics: AAA-паттерн: тесты которые ловят баги.
❌ Сейчас: «Мой код работал, а в production сломался!»
✅ После урока: «Я знаю, какие тесты писать, чтобы баги не доходили до пользователей»
Увидите разницу между «тест проходит» и «тест полезен»
Научитесь думать как «ломатель»: граничные случаи и неверные данные
Поймёте AAA как инструмент мышления, а не просто форматирование
# ❌ Баг: 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_000def 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() == []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)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Держите под рукой, когда пишете тесты.
None?test_что_проверяем_при_каких_условияхБонус: покажите тест коллеге — если понял за 10 секунд, тест хороший.
Теперь вы не просто запускаете тесты — вы проверяете качество кода до того, как баги увидят пользователей. 🎯
Далее: Parametrize: один тест — десятки сценариев