Fixture — a known, fixed state the database or system is in before a test
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
// 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();