Структура теста: паттерн AAA
Как структурировать тело теста, чтобы его было легко читать и поддерживать.
AAA (Arrange-Act-Assert) — паттерн из трёх шагов: подготовить данные, выполнить действие, проверить результат.
Три части любого теста
Хороший тест читается как маленькая история из трёх частей:
- Arrange (подготовка). Создаём входные данные, объекты, состояние — всё, что нужно для проверки.
- Act (действие). Вызываем ту самую функцию или метод, которую тестируем. Обычно это одна строка.
- Assert (проверка). Сравниваем фактический результат с ожидаемым через assert-методы.
Разделение на три блока делает тест предсказуемым: сразу видно, что подготовили, что вызвали и что ожидаем.
Пример с явным AAA
import unittest
def apply_discount(price, percent):
return round(price * (1 - percent / 100), 2)
class TestDiscount(unittest.TestCase):
def test_ten_percent_off(self):
# Arrange — готовим входные данные
price = 200
percent = 10
# Act — выполняем действие
result = apply_discount(price, percent)
# Assert — проверяем результат
self.assertEqual(result, 180.0)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_ten_percent_off (__main__.TestDiscount.test_ten_percent_off) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Комментарии Arrange/Act/Assert писать необязательно, но в начале они помогают держать структуру в голове. Со временем три блока становятся видны просто по пустым строкам между ними.
Правило: один тест — одно поведение
Каждый test_* метод должен проверять одну идею. Если в одном методе пять разных проверок и одна падает, остальные даже не выполнятся, и вы не узнаете их статус. Лучше разбить на отдельные тесты с говорящими именами:
import unittest
def apply_discount(price, percent):
return round(price * (1 - percent / 100), 2)
class TestDiscount(unittest.TestCase):
def test_no_discount(self):
self.assertEqual(apply_discount(100, 0), 100.0)
def test_half_off(self):
self.assertEqual(apply_discount(100, 50), 50.0)
def test_full_discount(self):
self.assertEqual(apply_discount(100, 100), 0.0)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_full_discount (__main__.TestDiscount.test_full_discount) ... ok test_half_off (__main__.TestDiscount.test_half_off) ... ok test_no_discount (__main__.TestDiscount.test_no_discount) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK
Теперь в отчёте видно каждый случай по отдельности, а имя теста читается как утверждение: «no discount», «half off», «full discount».
Как называть тесты
Имя теста — это документация. Сравните:
| Плохо | Хорошо |
test_1 | test_discount_zero_returns_full_price |
test_func | test_empty_list_returns_zero |
Хорошее имя описывает условие и ожидаемый результат. Когда тест падает, по одному имени понятно, что именно сломалось.
Распространённый шаблон именования — test_<что_делаем>_<при_каком_условии>_<что_ожидаем>. Например, test_login_with_wrong_password_returns_false. Имя получается длинным, и это хорошо: в отчёте CI вы видите его без открытия кода и сразу понимаете суть провала. Не экономьте на длине имени теста — он читается чаще, чем пишется.
Сколько assert'ов на тест
Правило «один тест — одно поведение» не означает «ровно один assert». В тесте может быть несколько проверок, если все они про одно и то же поведение. Например, после снятия денег со счёта логично проверить и новый баланс, и факт записи в историю операций — это один сценарий «успешное снятие». А вот смешивать в одном тесте проверку успешного снятия и проверку отказа при нехватке средств не стоит: это два разных поведения и два разных теста. Ориентируйтесь на смысл, а не на число строк assert.
Итог
- AAA: Arrange (подготовка) → Act (действие) → Assert (проверка).
- Один тест проверяет одно поведение — так понятнее отчёт и причина поломки.
- Имя теста должно описывать условие и ожидаемый результат.
- Пустые строки между блоками AAA делают тест читаемым без комментариев.