Структура теста: паттерн AAA

Как структурировать тело теста, чтобы его было легко читать и поддерживать.

AAA (Arrange-Act-Assert) — паттерн из трёх шагов: подготовить данные, выполнить действие, проверить результат.

Три части любого теста

Хороший тест читается как маленькая история из трёх частей:

  1. Arrange (подготовка). Создаём входные данные, объекты, состояние — всё, что нужно для проверки.
  2. Act (действие). Вызываем ту самую функцию или метод, которую тестируем. Обычно это одна строка.
  3. 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_1test_discount_zero_returns_full_price
test_functest_empty_list_returns_zero

Хорошее имя описывает условие и ожидаемый результат. Когда тест падает, по одному имени понятно, что именно сломалось.

Распространённый шаблон именования — test_<что_делаем>_<при_каком_условии>_<что_ожидаем>. Например, test_login_with_wrong_password_returns_false. Имя получается длинным, и это хорошо: в отчёте CI вы видите его без открытия кода и сразу понимаете суть провала. Не экономьте на длине имени теста — он читается чаще, чем пишется.

Сколько assert'ов на тест

Правило «один тест — одно поведение» не означает «ровно один assert». В тесте может быть несколько проверок, если все они про одно и то же поведение. Например, после снятия денег со счёта логично проверить и новый баланс, и факт записи в историю операций — это один сценарий «успешное снятие». А вот смешивать в одном тесте проверку успешного снятия и проверку отказа при нехватке средств не стоит: это два разных поведения и два разных теста. Ориентируйтесь на смысл, а не на число строк assert.

Итог

  • AAA: Arrange (подготовка) → Act (действие) → Assert (проверка).
  • Один тест проверяет одно поведение — так понятнее отчёт и причина поломки.
  • Имя теста должно описывать условие и ожидаемый результат.
  • Пустые строки между блоками AAA делают тест читаемым без комментариев.
Проверьте себя
1. Как расшифровывается AAA в структуре теста?
AAssert-Assign-Assert
BArrange-Act-Assert
CAdd-Apply-Assert
DAsync-Await-Assert
2. Почему лучше проверять одно поведение в одном test_-методе?
AТак быстрее выполняется
BЕсли одна проверка упадёт, остальные в том же методе не выполнятся, и их статус неизвестен
Cunittest запрещает несколько assert
DИначе тесты не находятся
3. Какое имя теста лучше?
Atest_1
Btest_func
Ctest_empty_list_returns_zero
Dtest_ok
Поддержать проект