P
imvdmolen.nl
Blog

Laravel's database seeding optimaliseren met Factory States voor complexe testdata

Testdata aanmaken voor projecten met meerdere gebruikersrollen, orderstatussen en productcategorieën kost onnodig veel tijd als je het handmatig doet. Zeker bij webshops waar je combinaties wilt testen die in de praktijk lastig te reproduceren zijn. Laravel's Factory States lossen dit probleem op: je definieert variaties van je models op één plek, en roept ze aan waar je ze nodig hebt.

Factory States zijn een krachtige uitbreiding op Laravel's standaard model factories die het mogelijk maken om verschillende configuraties van hetzelfde model te creëren. Waar een normale factory één standaard set attributen genereert, kun je met states specifieke aanpassingen definiëren die bovenop de basisattributen worden toegepast. Dit zorgt voor veel flexibelere en herbruikbare testdata generatie.

States definiëren in je Model Factory

Een factory state definieer je door de state method te gebruiken binnen je factory class. Stel je hebt een User model met verschillende rollen, dan kun je states maken voor admin gebruikers, gedeactiveerde accounts en premium subscribers. Elke state wijzigt alleen de attributen die specifiek zijn voor die configuratie.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    public function definition()
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => bcrypt('password'),
            'role' => 'user',
            'status' => 'active',
            'premium_until' => null,
        ];
    }

    public function admin()
    {
        return $this->state(function (array $attributes) {
            return [
                'role' => 'admin',
                'email_verified_at' => now(),
            ];
        });
    }

    public function inactive()
    {
        return $this->state(function (array $attributes) {
            return [
                'status' => 'inactive',
                'email_verified_at' => null,
            ];
        });
    }

    public function premium()
    {
        return $this->state(function (array $attributes) {
            return [
                'premium_until' => now()->addYear(),
                'role' => 'premium_user',
            ];
        });
    }
}

Deze aanpak is veel efficiënter dan het maken van aparte factory classes voor elke gebruikersvariant. Bovendien kun je states combineren om nog complexere scenario's te creëren. Een admin gebruiker met premium status maak je door beide states te ketenen: User::factory()->admin()->premium()->create().

Geavanceerde State Combinaties voor Complexe Relaties

States worden nog krachtiger wanneer je werkt met models die relaties hebben. Recent implementeerde ik een blog systeem waarbij artikelen verschillende publicatiestatus hadden en gekoppeld waren aan categorieën. Door states te gebruiken kon ik eenvoudig artikelen genereren die gepubliceerd waren, in concept stonden of gearchiveerd waren, elk met de juiste gerelateerde data.

<?php

namespace Database\Factories;

use App\Models\Category;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition()
    {
        return [
            'title' => fake()->sentence(),
            'content' => fake()->paragraphs(3, true),
            'status' => 'draft',
            'published_at' => null,
            'user_id' => User::factory(),
            'category_id' => Category::factory(),
            'view_count' => 0,
            'featured' => false,
        ];
    }

    public function published()
    {
        return $this->state(function (array $attributes) {
            return [
                'status' => 'published',
                'published_at' => fake()->dateTimeBetween('-30 days', 'now'),
                'view_count' => fake()->numberBetween(10, 1000),
            ];
        });
    }

    public function featured()
    {
        return $this->state(function (array $attributes) {
            return [
                'featured' => true,
                'status' => 'published',
                'published_at' => fake()->dateTimeBetween('-7 days', 'now'),
                'view_count' => fake()->numberBetween(500, 5000),
            ];
        });
    }

    public function archived()
    {
        return $this->state(function (array $attributes) {
            return [
                'status' => 'archived',
                'published_at' => fake()->dateTimeBetween('-1 year', '-3 months'),
                'view_count' => fake()->numberBetween(50, 2000),
            ];
        });
    }

    public function withComments()
    {
        return $this->has(
            \App\Models\Comment::factory()->count(fake()->numberBetween(1, 10)),
            'comments'
        );
    }
}

Wat hier interessant is, is hoe de featured state automatisch de status op published zet en realistische view counts genereert. Je kunt nu complexe scenario's maken zoals Post::factory()->featured()->withComments()->create() om een uitgelicht artikel met reacties te genereren.

States Gebruiken in Database Seeders

Database seeders worden veel overzichtelijker wanneer je states gebruikt in plaats van handmatig attributen te specificeren. Hier zie je hoe ik een realistische dataset voor een e-commerce applicatie genereer met verschillende product types, gebruikersrollen en order statussen.

<?php

namespace Database\Seeders;

use App\Models\User;
use App\Models\Product;
use App\Models\Order;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // Verschillende gebruikerstypes
        $adminUsers = User::factory()->count(2)->admin()->create();
        $premiumUsers = User::factory()->count(10)->premium()->create();
        $regularUsers = User::factory()->count(50)->create();
        $inactiveUsers = User::factory()->count(5)->inactive()->create();

        // Productcatalogus met verschillende states
        $featuredProducts = Product::factory()->count(5)->featured()->create();
        $discountProducts = Product::factory()->count(10)->onSale()->create();
        $outOfStockProducts = Product::factory()->count(3)->outOfStock()->create();
        $regularProducts = Product::factory()->count(30)->create();

        // Orders met realistische combinaties
        foreach ($premiumUsers->take(5) as $user) {
            Order::factory()->count(rand(1, 3))
                ->for($user)
                ->completed()
                ->create();
        }

        foreach ($regularUsers->take(20) as $user) {
            Order::factory()->count(rand(1, 2))
                ->for($user)
                ->pending()
                ->create();
        }
    }
}

Deze aanpak zorgt ervoor dat je seeders leesbaar blijven en snel verschillende scenario's kunt testen. Wanneer een collega of toekomstige ik deze code bekijkt, is meteen duidelijk wat voor data er wordt gegenereerd zonder door complexe array definities te hoeven spitten.

Dynamische States voor Tijdsafhankelijke Data

Een uitdaging waar ik vaak tegenaan loop is het genereren van tijdsafhankelijke testdata die realistisch blijft naarmate de tijd verstrijkt. States kunnen dynamische waarden gebruiken die telkens opnieuw worden berekend wanneer de factory wordt uitgevoerd. Dit is bijzonder nuttig voor subscription models, tijdelijke aanbiedingen of status overgangen.

public function expiringSoon()
{
    return $this->state(function (array $attributes) {
        return [
            'subscription_status' => 'active',
            'subscription_expires_at' => now()->addDays(rand(1, 7)),
            'renewal_reminder_sent' => fake()->boolean(30),
        ];
    });
}

public function recentlyExpired()
{
    return $this->state(function (array $attributes) {
        return [
            'subscription_status' => 'expired',
            'subscription_expires_at' => now()->subDays(rand(1, 30)),
            'grace_period_until' => now()->addDays(rand(1, 7)),
        ];
    });
}

public function trialUser()
{
    return $this->state(function (array $attributes) {
        $trialStart = fake()->dateTimeBetween('-14 days', 'now');
        
        return [
            'subscription_status' => 'trial',
            'trial_started_at' => $trialStart,
            'trial_ends_at' => (clone $trialStart)->modify('+14 days'),
            'subscription_expires_at' => null,
        ];
    });
}

Deze dynamische states zijn onmisbaar geweest tijdens het testen van subscription functionaliteit. Telkens wanneer ik de seeder draai, krijg ik gebruikers in verschillende fasen van hun abonnement zonder dat ik handmatig datums moet aanpassen.

States hebben mijn workflow aanzienlijk verbeterd door de complexiteit van testdata generatie weg te nemen. Waar ik vroeger veel tijd kwijt was aan het handmatig configureren van verschillende scenario's, kan ik nu met een paar regels code realistische datasets genereren die precies passen bij wat ik aan het testen ben. Het mooie is dat andere developers in het team deze states ook kunnen hergebruiken, wat zorgt voor consistentie in onze test omgevingen.