Database testing: factories, seeders e refreshDatabase

1. Introdução ao Database Testing no Laravel

Testar a camada de banco de dados é uma das práticas mais importantes no desenvolvimento de aplicações PHP modernas, especialmente quando utilizamos o Laravel. Sem testes adequados, regras de negócio que dependem de consultas SQL, relacionamentos e integridade referencial podem falhar silenciosamente em produção.

O Laravel oferece um ecossistema maduro para testes de banco de dados: Factories para gerar dados de forma rápida e realista, Seeders para popular o banco com dados iniciais e Traits como RefreshDatabase que gerenciam o ciclo de vida das migrações durante os testes.

Antes de começar, certifique-se de que seu arquivo phpunit.xml está configurado com um banco de dados de teste, geralmente SQLite em memória:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

2. Trait RefreshDatabase e Estratégias de Migração

A trait RefreshDatabase é uma das ferramentas mais poderosas para testes de banco no Laravel. Ela funciona em duas etapas:

  1. Executa todas as migrações antes do primeiro teste da classe
  2. Envolve cada teste em uma transação que é revertida ao final
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_create_user()
    {
        // O banco é migrado automaticamente
        // Cada teste roda dentro de uma transação
        $response = $this->post('/users', [
            'name' => 'João Silva',
            'email' => 'joao@example.com'
        ]);

        $response->assertStatus(201);
        $this->assertDatabaseHas('users', ['email' => 'joao@example.com']);
    }
}

Diferenças entre RefreshDatabase e DatabaseMigrations:

  • RefreshDatabase: migra uma vez e usa transações — mais rápido para múltiplos testes
  • DatabaseMigrations: executa migrate:fresh antes de cada teste — mais lento, mas isolado

Boas práticas:
- Use RefreshDatabase como padrão para testes de feature
- Use DatabaseMigrations apenas se seus testes precisarem de isolamento completo de migrações
- Para testes unitários que não tocam no banco, não use nenhuma das duas

3. Factories: Definição e Uso Básico

Factories são definições de como criar modelos com dados falsos, mas realistas. Crie uma factory com o comando Artisan:

php artisan make:factory UserFactory --model=User

A estrutura básica de uma factory utiliza a biblioteca Faker:

<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    protected $model = User::class;

    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
            'remember_token' => Str::random(10),
        ];
    }
}

Gerando registros:

// Cria e persiste no banco
$user = User::factory()->create();

// Cria mas não persiste (útil para testes de validação)
$user = User::factory()->make();

// Cria múltiplos registros
$users = User::factory()->count(10)->create();

4. Factories Avançadas: Relacionamentos e Sequências

Definindo relacionamentos entre factories

<?php

namespace Database\Factories;

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

class PostFactory extends Factory
{
    protected $model = Post::class;

    public function definition(): array
    {
        return [
            'title' => fake()->sentence(),
            'body' => fake()->paragraphs(3, true),
            'user_id' => User::factory(), // Cria um usuário automaticamente
        ];
    }

    // Estado para post publicado
    public function published(): static
    {
        return $this->state(fn (array $attributes) => [
            'published_at' => now(),
        ]);
    }
}

Usando sequence() para dados variados em lote

$users = User::factory()
    ->count(5)
    ->sequence(
        ['role' => 'admin'],
        ['role' => 'editor'],
        ['role' => 'subscriber'],
    )
    ->create();

Callbacks de factory

public function configure(): static
{
    return $this->afterCreating(function (User $user) {
        // Cria perfil automaticamente após criar usuário
        $user->profile()->create([
            'bio' => fake()->paragraph()
        ]);
    });
}

5. Seeders: Organização e Execução de Dados de Teste

Seeders permitem popular o banco com dados iniciais de forma organizada. Crie um seeder:

php artisan make:seeder UserSeeder
<?php

namespace Database\Seeders;

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

class UserSeeder extends Seeder
{
    public function run(): void
    {
        User::factory()
            ->count(50)
            ->hasPosts(5) // Cada usuário terá 5 posts
            ->create();
    }
}

Ordem de execução com DatabaseSeeder:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            RoleSeeder::class,
            UserSeeder::class,   // Depende de roles existentes
            PostSeeder::class,   // Depende de usuários existentes
        ]);
    }
}

6. Testando com Factories e Seeders na Prática

Escrevendo testes que usam factories

<?php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_view_own_posts()
    {
        $user = User::factory()
            ->has(Post::factory()->count(3))
            ->create();

        $response = $this->actingAs($user)
            ->get('/my-posts');

        $response->assertStatus(200);
        $response->assertSee($user->posts->first()->title);
    }

    public function test_database_has_expected_data()
    {
        Post::factory()->create(['title' => 'Artigo Teste']);

        $this->assertDatabaseHas('posts', [
            'title' => 'Artigo Teste'
        ]);

        $this->assertDatabaseMissing('posts', [
            'title' => 'Artigo Inexistente'
        ]);
    }
}

Testando com seeders

public function test_application_seeded_correctly()
{
    // Executa todos os seeders do DatabaseSeeder
    $this->seed();

    // Ou executa um seeder específico
    $this->seed(RoleSeeder::class);

    $this->assertDatabaseHas('roles', ['name' => 'admin']);
}

7. Dicas Avançadas e Troubleshooting

Evitando dados duplicados

// Usando firstOrCreate em estados
public function admin(): static
{
    return $this->state(fn (array $attributes) => [
        'email' => 'admin@example.com',
        'role' => 'admin',
    ]);
}

Debugando factories

public function definition(): array
{
    $data = [
        'name' => fake()->name(),
        'email' => fake()->email(),
    ];

    // Descomente para depuração
    // dd($data);

    return $data;
}

Performance: limitando criações em lote

// Ruim para performance
User::factory()->count(1000)->create();

// Melhor: usar make() quando possível e salvar depois
$users = User::factory()->count(1000)->make();
foreach ($users as $user) {
    $user->save();
}

// Ou usar chunks
User::factory()->count(1000)->create()->each(function ($user) {
    // Processa em lotes menores
});

Dica importante: Sempre use make() em vez de create() quando o teste não precisar persistir o registro. Isso evita escrita desnecessária no banco e acelera os testes.

Referências