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:
- Executa todas as migrações antes do primeiro teste da classe
- 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 testesDatabaseMigrations: executamigrate:freshantes 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
- Laravel Documentation: Database Testing — Documentação oficial do Laravel sobre testes de banco de dados, incluindo factories, seeders e traits
- Laravel Documentation: Eloquent Factories — Guia completo sobre criação e uso de factories no Laravel
- Laravel Documentation: Seeding — Documentação oficial sobre seeders e como popular bancos de dados
- Laravel News: Database Testing Best Practices — Artigo com dicas avançadas e boas práticas para testar banco de dados no Laravel
- Laravel Daily: Testing with Factories and Seeders — Tutorial prático com exemplos reais de uso de factories e seeders em testes
- FakerPHP Documentation — Documentação oficial da biblioteca Faker, usada internamente pelas factories do Laravel