AAA-паттерн: тесты которые ловят баги
Вы уже умеете писать тесты. Но работающий тест и хороший тест — это разные вещи.
Цель: Научиться писать тесты, которые легко читать, поддерживать и которые находят баги раньше, чем их увидят пользователи.
Вы точно готовы?
Убедитесь, что вы прошли предыдущие уроки:
# Вы должны уметь написать такой тест
def test_user_age():
user = User("Alice", 25)
assert user.is_adult() == TrueЕсли что-то непонятно — вернитесь к урокам 0-2.
Проблема: плохие тесты
Реальный пример плохого теста:
def test_stuff():
u = User("Alice", 25)
assert u.greet() == "Hello, I'm Alice!"
u.celebrate_birthday()
assert u.age == 26
u2 = User("Bob", 16)
assert not u2.is_adult()
# ... ещё 10 assert'овЧто не так:
- ❌ Непонятное название (
test_stuff) - ❌ Проверяет сразу несколько вещей
- ❌ Когда тест падает — непонятно ЧТО сломалось
- ❌ Сложно модифицировать
Хороший тест должен:
- ✅ Иметь понятное название
- ✅ Проверять ОДНУ концепцию
- ✅ Чётко показывать ЧТО сломалось
- ✅ Легко читаться и поддерживаться
AAA Pattern: Arrange-Act-Assert
AAA — это структура теста из 3 частей:
- Arrange (Подготовка) — создаём данные
- Act (Действие) — вызываем тестируемый код
- Assert (Проверка) — проверяем результат
Плохой тест: всё смешано
def test_bad():
user = User("Alice", 25)
greeting = user.greet()
assert greeting == "Hello, I'm Alice!"
user.celebrate_birthday()
assert user.age == 26Проблема: Непонятно где setup, где действие, где проверка.
Хороший тест: чёткая структура
def test_user_greeting():
# Arrange: подготовка данных
user = User("Alice", 25)
# Act: выполнение действия
greeting = user.greet()
# Assert: проверка результата
assert greeting == "Hello, I'm Alice!"Почему лучше:
- ✅ Видно ЧТО тестируем (
greet()) - ✅ Видно КАК готовим данные (создаём пользователя)
- ✅ Видно ЧТО проверяем (приветствие)
Ещё примеры AAA
def test_bank_account_deposit():
# Arrange
account = BankAccount(initial_balance=100)
# Act
new_balance = account.deposit(50)
# Assert
assert new_balance == 150
assert account.balance == 150
def test_bank_account_insufficient_funds():
# Arrange
account = BankAccount(initial_balance=50)
# Act & Assert (для исключений можно объединить)
with pytest.raises(ValueError) as exc_info:
account.withdraw(100)
assert "Insufficient funds" in str(exc_info.value)Понятные assert messages
Проблема: непонятные ошибки
Плохой assert:
def test_discount():
price = calculate_discount(100, 0.1)
assert price == 90Когда тест падает:
AssertionError: assert 91 == 90Непонятно:
- Что за 91?
- Что за 90?
- Откуда взялась скидка 0.1?
Решение: assert с сообщением
def test_discount_with_message():
# Arrange
original_price = 100
discount_rate = 0.1
expected_price = 90
# Act
actual_price = calculate_discount(original_price, discount_rate)
# Assert
assert actual_price == expected_price, (
f"Discount calculation failed: "
f"expected {expected_price} but got {actual_price} "
f"for price {original_price} with {discount_rate * 100}% discount"
)Когда тест падает:
AssertionError: Discount calculation failed: expected 90 but got 91
for price 100 with 10.0% discount✅ Теперь понятно ЧТО пошло не так!
Примеры хороших assert messages
def test_user_adult_status():
# Arrange
user = User("Alice", 17)
# Act
is_adult = user.is_adult()
# Assert
assert is_adult == False, (
f"User with age {user.age} should NOT be adult"
)
def test_list_contains_item():
# Arrange
shopping_list = ["apples", "bread", "milk"]
item = "bread"
# Act & Assert
assert item in shopping_list, (
f"Expected '{item}' to be in {shopping_list}"
)One Concept Per Test
Проблема: тест проверяет слишком много
# ❌ ПЛОХО — тестирует И greeting, И birthday, И adult status
def test_user_all_methods():
user = User("Alice", 17)
# Проверяем greeting
assert user.greet() == "Hello, I'm Alice!"
# Проверяем birthday
user.celebrate_birthday()
assert user.age == 18
# Проверяем adult status
assert user.is_adult() == TrueКогда тест падает на greet():
- ❌ Остальные проверки НЕ запустятся
- ❌ Непонятно, сломан только
greet()или что-то ещё
Решение: отдельный тест для каждой концепции
# ✅ ХОРОШО — каждый тест проверяет ОДНУ вещь
def test_user_greeting():
"""Тест приветствия"""
# Arrange
user = User("Alice", 17)
# Act
greeting = user.greet()
# Assert
assert greeting == "Hello, I'm Alice!"
def test_user_celebrates_birthday():
"""Тест увеличения возраста"""
# Arrange
user = User("Alice", 17)
# Act
new_age = user.celebrate_birthday()
# Assert
assert new_age == 18
assert user.age == 18
def test_user_becomes_adult_after_birthday():
"""Тест: после 18-го дня рождения становится взрослым"""
# Arrange
user = User("Alice", 17)
# Act
user.celebrate_birthday() # Теперь 18
# Assert
assert user.is_adult() == True, (
f"User should be adult after turning {user.age}"
)Преимущества:
- ✅ Если
greet()сломан — другие тесты всё равно запустятся - ✅ По названию теста понятно ЧТО сломалось
- ✅ Легко добавить новые тесты
Именование тестов: описывайте поведение
Плохие названия
# ❌ ПЛОХО
def test_1():
pass
def test_user():
pass
def test_function():
passПроблема: Не понятно ЧТО тестируется.
Хорошие названия
# ✅ ХОРОШО — описывает ПОВЕДЕНИЕ
def test_user_greets_with_their_name():
"""Приветствие содержит имя пользователя"""
pass
def test_deposit_increases_balance():
"""Пополнение увеличивает баланс"""
pass
def test_withdraw_fails_when_insufficient_funds():
"""Снятие выбрасывает ошибку при недостатке средств"""
pass
def test_user_becomes_adult_at_18():
"""Пользователь становится взрослым в 18 лет"""
passFormula для названий
Pattern: test_[что]_[делает]_[при_каких_условиях]
Примеры:
test_discount_applies_correctly_for_vip_userstest_login_fails_with_wrong_passwordtest_email_sends_when_order_confirmedtest_cache_expires_after_one_hour
Практический пример: до и после
До: плохой тест
def test_shopping_cart():
cart = ShoppingCart()
cart.add_item("apple", 1.50)
cart.add_item("bread", 2.00)
assert cart.total() == 3.50
cart.apply_discount(0.1)
assert cart.total() == 3.15
cart.remove_item("apple")
assert cart.total() == 1.80После: хорошие тесты
def test_empty_cart_has_zero_total():
"""Пустая корзина имеет итог 0"""
# Arrange
cart = ShoppingCart()
# Act
total = cart.total()
# Assert
assert total == 0, "Empty cart should have total of 0"
def test_adding_items_increases_total():
"""Добавление товаров увеличивает итог"""
# Arrange
cart = ShoppingCart()
# Act
cart.add_item("apple", price=1.50)
cart.add_item("bread", price=2.00)
total = cart.total()
# Assert
assert total == 3.50, (
f"Expected total 3.50 for apple(1.50) + bread(2.00), got {total}"
)
def test_discount_reduces_total_by_percentage():
"""Скидка уменьшает итог на процент"""
# Arrange
cart = ShoppingCart()
cart.add_item("apple", price=1.50)
cart.add_item("bread", price=2.00)
original_total = 3.50
discount = 0.1 # 10%
expected_total = 3.15
# Act
cart.apply_discount(discount)
actual_total = cart.total()
# Assert
assert actual_total == expected_total, (
f"10% discount on {original_total} should give {expected_total}, "
f"but got {actual_total}"
)
def test_removing_item_decreases_total():
"""Удаление товара уменьшает итог"""
# Arrange
cart = ShoppingCart()
cart.add_item("apple", price=1.50)
cart.add_item("bread", price=2.00)
cart.apply_discount(0.1) # Total now 3.15
# Act
cart.remove_item("apple")
total = cart.total()
# Assert
# After removing apple (1.35 after discount), only bread remains (1.80)
assert total == 1.80, (
f"After removing discounted apple (1.35), "
f"only bread (1.80) should remain, got {total}"
)Что вы изучили
- AAA Pattern — структура Arrange-Act-Assert
- Понятные assert messages — сообщения для отладки
- One concept per test — один тест = одна проверка
- Хорошие названия тестов — описание поведения
- Тесты как документация — код объясняет сам себя
Следующий урок
Отлично! Теперь вы пишете чистые тесты. Но что если нужно протестировать одну функцию с 10 разными входными данными? Копировать тест 10 раз?
Переходите к уроку 4: Parametrize: один тест — десятки сценариев
В следующем уроке вы узнаете:
@pytest.mark.parametrizeдля multiple inputs- Как сократить 10 тестов до одного
- Именование параметризованных тестов
- Когда НЕ использовать parametrize
Устранение неисправностей
Держите один тест для одной концепции и делите сценарии
Проверьте:
- Разбейте большой тест на несколько мелких по поведению
- Используйте AAA-структуру, чтобы ясно видеть Arrange/Act/Assert
Давайте осмысленные названия, описывающие поведение и условия
Проверьте:
- Следуйте шаблону: test_[что]_[делает]_[при_условиях]
- Оставляйте тест сфокусированным на одном ожидании
Добавьте сообщение в assert с ожидаемым и фактическим значениями
Проверьте:
- Формулируйте контекст: f"Expected {expected} but got {actual}"
- Включайте входные данные в сообщение, если это помогает отладке
Вынесите повторяющийся setup в фикстуры — подробно в уроке 6
Проверьте:
- Создайте общие функции/фикстуры для подготовки данных
- Подключайте их в тестах вместо копипасты