0

Fixture — a known, fixed state the database or system is in before a test

Beginner5 min read·eng-17-009

Concept

Fixture — a known, fixed state that the system is in before a test runs. Fixtures set up the preconditions: specific database records, files, configuration, or any state the test depends on.

Why fixtures are needed: Tests need a consistent starting point. If a test checks "can a premium user access the dashboard?", it needs a premium user to exist. The fixture creates that user.

Types of fixtures:

  • Database fixtures: Specific rows in the DB. Laravel factories + setUp().
  • File fixtures: Test files in a tests/fixtures/ directory (CSV, JSON, XML files).
  • Object fixtures: Pre-built objects used across tests.
  • Configuration fixtures: Specific config values for the test context.

The problem with shared fixtures: If one test modifies a shared fixture, the next test may fail because the state has changed. Solution: reset state between tests.

Laravel approaches to DB fixtures:

  • RefreshDatabase: Runs all migrations, then wraps each test in a transaction and rolls back. Clean state per test.
  • DatabaseTransactions: Only wraps in a transaction. Faster but doesn't re-run migrations.
  • DatabaseMigrations: Re-runs migrations before the test suite. Slower.

Factories as fixtures (Laravel 8+): User::factory()->create() — creates a specific user. User::factory()->premium()->withOrders(5)->create() — complex fixture via factory states.

setUp() method: PHPUnit's method that runs before each test. Ideal for creating fixtures.

Fixture isolation: Each test should create its own fixtures, not depend on fixtures created by previous tests. Test order must not matter.

Code Example

php
<?php
// DATABASE FIXTURE via factories
class DashboardTest extends \Tests\TestCase
{
    use \Illuminate\Foundation\Testing\RefreshDatabase; // clean DB per test

    private \App\Models\User $premiumUser;
    private \App\Models\User $freeUser;

    protected function setUp(): void
    {
        parent::setUp();

        // Create fixtures for this test class
        $this->premiumUser = \App\Models\User::factory()
            ->premium()             // factory state: sets subscription_plan
            ->create(['name' => 'Alice Premium']);

        $this->freeUser = \App\Models\User::factory()
            ->create(['name' => 'Bob Free']);

        // Create related data fixtures
        \App\Models\Order::factory()
            ->count(3)
            ->for($this->premiumUser)
            ->create(['status' => 'paid']);
    }

    public function test_premium_user_sees_dashboard(): void
    {
        $this->actingAs($this->premiumUser)
             ->getJson('/api/dashboard')
             ->assertStatus(200)
             ->assertJsonFragment(['order_count' => 3]);
    }

    public function test_free_user_is_redirected(): void
    {
        $this->actingAs($this->freeUser)
             ->getJson('/api/dashboard')
             ->assertStatus(403);
    }
}

// FILE FIXTURES — for parsing or import tests
class CsvImporterTest extends \PHPUnit\Framework\TestCase
{
    private string $fixturePath;

    protected function setUp(): void
    {
        $this->fixturePath = __DIR__ . '/fixtures/';
    }

    public function test_imports_valid_csv(): void
    {
        // tests/fixtures/users.csv:
        // id,name,email
        // 1,Alice,alice@example.com
        // 2,Bob,bob@example.com
        $importer = new CsvImporter();
        $result   = $importer->import($this->fixturePath . 'users.csv');

        $this->assertCount(2, $result);
        $this->assertEquals('Alice', $result[0]['name']);
    }

    public function test_handles_malformed_csv(): void
    {
        // tests/fixtures/malformed.csv — missing headers, wrong delimiter
        $this->expectException(\App\Exceptions\ImportException::class);
        (new CsvImporter())->import($this->fixturePath . 'malformed.csv');
    }
}

// FACTORY STATES as fixture builders
// In UserFactory:
public function premium(): Factory
{
    return $this->state(['subscription_plan' => 'premium', 'subscription_started_at' => now()]);
}

public function suspended(): Factory
{
    return $this->state(['suspended_at' => now(), 'active' => false]);
}

public function withOrders(int $count = 5): Factory
{
    return $this->has(\App\Models\Order::factory()->count($count));
}

// Creating complex fixtures
$user = User::factory()->premium()->withOrders(3)->create();