Что такое паттерн AAA (Arrange-Act-Assert) в тестировании?

Ответ

AAA (Arrange-Act-Assert) — это структурный паттерн для организации unit-тестов, который разделяет тест на три четкие фазы, улучшая читаемость и поддерживаемость.

Структура AAA:

1. Arrange (Подготовка)

Настройка тестового окружения: создание объектов, мокирование зависимостей, подготовка входных данных.

2. Act (Действие)

Выполнение тестируемого метода или операции.

3. Assert (Проверка)

Верификация результатов: проверка возвращаемых значений, состояния объектов, вызовов зависимостей.

Пример на PHP с PHPUnit:

class UserServiceTest extends TestCase
{
    public function testUserIsActivatedAfterConfirmation(): void
    {
        // ===== ARRANGE =====
        // Подготовка всех необходимых объектов и данных
        $userRepository = $this->createMock(UserRepository::class);
        $emailService = $this->createMock(EmailService::class);

        $user = new User(
            id: 1,
            email: 'test@example.com',
            isActive: false,
            confirmationToken: 'abc123'
        );

        $userService = new UserService($userRepository, $emailService);

        // ===== ACT =====
        // Выполнение тестируемого действия
        $result = $userService->confirmEmail($user, 'abc123');

        // ===== ASSERT =====
        // Проверка всех ожидаемых результатов
        $this->assertTrue($result);
        $this->assertTrue($user->isActive());
        $this->assertNull($user->getConfirmationToken());
    }
}

Более сложный пример с моками:

public function testOrderProcessingChargesPayment(): void
{
    // ARRANGE
    $paymentGateway = $this->createMock(PaymentGateway::class);
    $inventory = $this->createMock(Inventory::class);

    // Настройка ожиданий для моков
    $paymentGateway->expects($this->once())
                   ->method('charge')
                   ->with(100.00, 'order_123')
                   ->willReturn(new PaymentResult(true, 'txn_456'));

    $inventory->expects($this->exactly(2))
              ->method('reserveItem');

    $order = new Order(id: 'order_123', total: 100.00);
    $order->addItem(new OrderItem(productId: 1, quantity: 2));
    $order->addItem(new OrderItem(productId: 2, quantity: 1));

    $orderProcessor = new OrderProcessor($paymentGateway, $inventory);

    // ACT
    $result = $orderProcessor->process($order);

    // ASSERT
    $this->assertTrue($result->isSuccess());
    $this->assertEquals('txn_456', $result->getTransactionId());
    $this->assertEquals(OrderStatus::PROCESSED, $order->getStatus());
}

Преимущества паттерна AAA:

1. Улучшенная читаемость:

// Плохо: смешанная логика
public function testSomething(): void
{
    $service = new Service();
    $result = $service->doSomething('input'); // Act среди Arrange
    $this->assertEquals('expected', $result);
    $dependency = new Dependency(); // Arrange после Act!
    // ...
}

// Хорошо: четкое разделение
public function testSomething(): void
{
    // Arrange - только подготовка
    $dependency = new Dependency();
    $service = new Service($dependency);

    // Act - одно действие
    $result = $service->doSomething('input');

    // Assert - только проверки
    $this->assertEquals('expected', $result);
}

2. Легкость отладки: При падении теста сразу понятно, на каком этапе проблема:

  • Ошибка в Arrange → проблема с подготовкой данных
  • Ошибка в Act → проблема в тестируемом коде
  • Ошибка в Assert → неверные ожидания

3. Соблюдение Single Responsibility Principle: Каждый тест проверяет одну конкретную функциональность.

4. Упрощение рефакторинга: Четкая структура делает тесты более устойчивыми к изменениям.

Рекомендации по использованию:

Один Act на тест:

// Плохо: несколько действий
public function testMultipleActions(): void
{
    // Arrange
    $calculator = new Calculator();

    // Act 1
    $sum = $calculator->add(2, 3);

    // Act 2 (нарушение паттерна)
    $product = $calculator->multiply($sum, 4);

    // Assert
    $this->assertEquals(20, $product);
}

// Хорошо: разделить на два теста
public function testAddition(): void { /* тестирует add() */ }
public function testMultiplication(): void { /* тестирует multiply() */ }

Минимальный Arrange:

// Плохо: избыточная подготовка
public function testSomething(): void
{
    // Arrange
    $config = new Config(['option1' => 'value1', 'option2' => 'value2']);
    $logger = new Logger();
    $cache = new Cache();
    $service = new Service($config, $logger, $cache);
    // ... но тест использует только $config
}

// Хорошо: только необходимые зависимости
public function testSomething(): void
{
    // Arrange
    $config = new Config(['option1' => 'value1']);
    $service = new Service($config);
    // ...
}

Альтернативные названия паттерна:

  • Given-When-Then (в BDD-стиле)
  • Setup-Exercise-Verify (в xUnit)

Паттерн AAA является де-факто стандартом для написания чистых, поддерживаемых unit-тестов в PHP и других языках.

Ответ 18+ 🔞

А, слушай, про AAA в тестах! Это же, блядь, святое, основа основ, без этого нихуя не работает нормально. Представь, что ты пишешь тест, а там всё в одну кучу свалено — подготовка, вызов, проверки. Читаешь такой код и думаешь: «Ёпта, что тут вообще происходит?» Так вот, AAA — это как раз чтобы такого пиздеца не было.

AAA (Arrange-Act-Assert) — это, грубо говоря, такой шаблон для организации юнит-тестов, который разбивает весь этот цирк на три понятных этапа. Читаемость зашкаливает, поддерживать потом не больно.

Из чего состоит эта магия:

1. Arrange (Подготовка, или «Расчехляемся»)

Тут мы всё готовим. Создаём объекты, настраиваем моки, подготавливаем данные — в общем, делаем всю подводку, чтобы тест мог стартануть. Доверия ебать ноль ко всему стороннему, поэтому мокаем всё, что шевелится.

2. Act (Действие, или «Стреляем»)

Один, блядь, четкий вызов того метода, который мы собственно тестируем. Никаких лишних движений! Одно действие — одна ответственность.

3. Assert (Проверка, или «Смотрим, куда попали»)

А вот тут уже проверяем результаты. Сошлось ли с ожиданием? Вызвался ли мок нужное количество раз? Всё ли в порядке с состоянием объекта после вызова?

Смотри, как это выглядит на PHP с PHPUnit:

class UserServiceTest extends TestCase
{
    public function testUserIsActivatedAfterConfirmation(): void
    {
        // ===== ARRANGE =====
        // Тащемта, готовим поле для игры
        $userRepository = $this->createMock(UserRepository::class);
        $emailService = $this->createMock(EmailService::class);

        $user = new User(
            id: 1,
            email: 'test@example.com',
            isActive: false,
            confirmationToken: 'abc123'
        );

        $userService = new UserService($userRepository, $emailService);

        // ===== ACT =====
        // Самый главный пинок
        $result = $userService->confirmEmail($user, 'abc123');

        // ===== ASSERT =====
        // Ну и что у нас получилось, а?
        $this->assertTrue($result);
        $this->assertTrue($user->isActive());
        $this->assertNull($user->getConfirmationToken());
    }
}

А вот пример посерьёзнее, с моками:

public function testOrderProcessingChargesPayment(): void
{
    // ARRANGE
    $paymentGateway = $this->createMock(PaymentGateway::class);
    $inventory = $this->createMock(Inventory::class);

    // Настраиваем этих мартышлюшек, чтобы делали то, что нам надо
    $paymentGateway->expects($this->once())
                   ->method('charge')
                   ->with(100.00, 'order_123')
                   ->willReturn(new PaymentResult(true, 'txn_456'));

    $inventory->expects($this->exactly(2))
              ->method('reserveItem');

    $order = new Order(id: 'order_123', total: 100.00);
    $order->addItem(new OrderItem(productId: 1, quantity: 2));
    $order->addItem(new OrderItem(productId: 2, quantity: 1));

    $orderProcessor = new OrderProcessor($paymentGateway, $inventory);

    // ACT
    $result = $orderProcessor->process($order);

    // ASSERT
    $this->assertTrue($result->isSuccess());
    $this->assertEquals('txn_456', $result->getTransactionId());
    $this->assertEquals(OrderStatus::PROCESSED, $order->getStatus());
}

Почему это, блядь, так охуенно:

1. Читаемость на уровне бога.

// Пиздец как плохо: всё в кучу
public function testSomething(): void
{
    $service = new Service();
    $result = $service->doSomething('input'); // Act среди Arrange, ёпта
    $this->assertEquals('expected', $result);
    $dependency = new Dependency(); // Ты че, больной? Arrange после Act!
    // ...
}

// А вот так — красота:
public function testSomething(): void
{
    // Arrange - только подготовка, чистота
    $dependency = new Dependency();
    $service = new Service($dependency);

    // Act - один меткий выстрел
    $result = $service->doSomething('input');

    // Assert - проверяем, не обосрались ли мы
    $this->assertEquals('expected', $result);
}

2. Отладка — одно удовольствие. Тест упал? Сразу видно, где ядрёна вошь:

  • Сломалось в Arrange — проблемы с данными или моками.
  • Сломалось в Act — косяк в самом тестируемом коде, сам от себя охуел.
  • Сломалось в Assert — наши ожидания не совпали с реальностью, пора э бошка думай.

3. Соблюдается принцип единой ответственности. Один тест — одна фича. Никаких «а заодно и вот это проверим».

4. Рефакторить потом в кайф. Чёткая структура делает тесты устойчивее к изменениям в коде.

Важные правила, которые лучше не нарушать:

Одно действие на тест — священная корова.

// Не делай так, пидарас шерстяной:
public function testMultipleActions(): void
{
    // Arrange
    $calculator = new Calculator();

    // Act 1
    $sum = $calculator->add(2, 3);

    // Act 2 (это уже пиздец, нарушение всех договорённостей)
    $product = $calculator->multiply($sum, 4);

    // Assert
    $this->assertEquals(20, $product);
}

// Раздели на два теста и спи спокойно:
public function testAddition(): void { /* тестируем только сложение */ }
public function testMultiplication(): void { /* тестируем только умножение */ }

Arrange должен быть минимальным.

// Не настраивай овердохуища всего, если не нужно:
public function testSomething(): void
{
    // Arrange
    $config = new Config(['option1' => 'value1', 'option2' => 'value2']);
    $logger = new Logger(); // А он в тесте не используется!
    $cache = new Cache();   // И этот тоже!
    $service = new Service($config, $logger, $cache);
    // ... зачем ты это сделал? Зачем?
}

// Готовь только то, что реально нужно для теста:
public function testSomething(): void
{
    // Arrange
    $config = new Config(['option1' => 'value1']);
    $service = new Service($config); // Всё, красота.
    // ...
}

Этот паттерн ещё иногда называют:

  • Given-When-Then (когда ты фанат BDD)
  • Setup-Exercise-Verify (старая школа xUnit)

Короче, AAA — это не просто паттерн, это, блядь, образ жизни для пишущего тесты. Без него получается пиздопроебибна, с ним — чистый, читаемый и надёжный код. Бери на вооружение и не выёбывайся.