0

Arrange-Act-Assert (AAA) — the universal structure of a well-written test

Beginner5 min read·eng-17-010
interview

Concept

Arrange-Act-Assert (AAA) — the universal 3-phase structure for writing clear, readable tests.

Arrange: Set up the state needed for the test. Create fixtures, configure mocks, prepare data. Act: Execute the code being tested. Call the method, make the HTTP request, run the function. Assert: Verify the result. Check return values, database state, method calls.

Why AAA matters:

  • Makes the test readable: Anyone can identify what's being tested just by scanning the three phases.
  • Makes failures clear: The assertion shows exactly what was expected vs what happened.
  • Prevents test code from becoming tangled logic — each phase is distinct.

Alternative names: "Given-When-Then" (BDD style) — same structure, different vocabulary. "Given" (Arrange), "When" (Act), "Then" (Assert).

One behavior per test: Each test should test ONE thing. If a test has many assertions, it might be testing too many behaviors. A failing test should tell you exactly what broke.

Common violations:

  • Arrange happens in the middle of the test.
  • Multiple Acts (testing two behaviors in one test).
  • Assert before Act (wrong order).
  • Giant tests with many behaviors — split them up.

Comments: Some developers add // Arrange, // Act, // Assert comments. Others prefer empty lines as separators. Either way, the structure should be clear.

Code Example

php
<?php
class PricingEngineTest extends \PHPUnit\Framework\TestCase
{
    private PricingEngine $engine;

    // Arrange (shared): setUp runs before each test
    protected function setUp(): void
    {
        $this->engine = new PricingEngine();
    }

    public function test_applies_percentage_discount(): void
    {
        // ARRANGE — set up specific inputs for this test
        $price           = 100.00;
        $discountPercent = 20.0;

        // ACT — run the code being tested
        $result = $this->engine->applyDiscount($price, $discountPercent);

        // ASSERT — verify the outcome
        $this->assertEquals(80.00, $result);
    }

    public function test_throws_when_discount_exceeds_100_percent(): void
    {
        // ARRANGE
        $price           = 50.00;
        $invalidDiscount = 150.0;

        // ASSERT (pre-arranged for exceptions — must be before ACT)
        $this->expectException(\InvalidArgumentException::class);

        // ACT — code throws, expectException captures it
        $this->engine->applyDiscount($price, $invalidDiscount);
    }
}

// FEATURE TEST with AAA
class OrderCreationTest extends \Tests\TestCase
{
    use \Illuminate\Foundation\Testing\RefreshDatabase;

    public function test_authenticated_user_can_create_order(): void
    {
        // ARRANGE — set up users, products, auth state
        $user    = User::factory()->create();
        $product = Product::factory()->create(['price' => 29.99, 'stock' => 10]);

        // ACT — make the HTTP request
        $response = $this->actingAs($user, 'sanctum')
                         ->postJson('/api/orders', [
                             'items' => [['product_id' => $product->id, 'quantity' => 2]],
                         ]);

        // ASSERT — check response and side effects
        $response->assertStatus(201)
                 ->assertJson(['status' => 'pending', 'total' => 59.98]);

        $this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 59.98]);
        $this->assertEquals(8, $product->fresh()->stock);
    }
}

// GIVEN-WHEN-THEN (BDD variant — same structure)
class UserRegistrationTest extends \Tests\TestCase
{
    public function test_new_user_can_register(): void
    {
        // GIVEN — a new visitor with unique email
        $email = 'new_user_' . uniqid() . '@example.com';

        // WHEN — they submit the registration form
        $response = $this->postJson('/api/register', [
            'name'                  => 'Alice',
            'email'                 => $email,
            'password'              => 'SecurePass123!',
            'password_confirmation' => 'SecurePass123!',
        ]);

        // THEN — they are registered and authenticated
        $response->assertStatus(201);
        $this->assertDatabaseHas('users', ['email' => $email]);
    }
}