Fábricas Laravel: Construindo Dados de Teste Realistas com Modelos de Fábrica Laravel
Ao desenvolver aplicações com Laravel, um dos gargalos mais comuns no fluxo de trabalho de testes é gerar dados significativos e realistas. As factories do Laravel são classes que definem um modelo para criar instâncias de modelos Eloquent, utilizando a biblioteca PHP Faker para produzir valores de atributos aleatórios mas estruturalmente válidos — permitindo que os desenvolvedores populem bases de dados e escrevam testes isolados sem construir manualmente fixtures de dados.
Ao contrário de ficheiros SQL estáticos ou arrays codificados, as factories são combináveis, com estado e conscientes de relacionamentos. Integram-se diretamente com as suites de testes PHPUnit e Pest, suportam avaliação lazy de atributos e escalam desde uma única instância de modelo até milhares de registos numa única cadeia de métodos. Se estiver a executar o Laravel num ambiente de VPS Hosting, as factories tornam-se especialmente valiosas durante execuções de pipelines CI/CD, reposições de ambientes de staging e cenários de testes de carga onde a geração de dados repetível e controlada é indispensável.
O Que São as Factories do Laravel e Por Que São Importantes
As factories do Laravel foram fundamentalmente redesenhadas no Laravel 8. A abordagem mais antiga baseada em closures `$factory->define()` foi substituída por classes PHP dedicadas que estendem `IlluminateDatabaseEloquentFactoriesFactory`. Esta mudança arquitetural introduziu segurança de tipos, autocompleção em IDE e uma separação mais clara entre a lógica das factories e as definições dos modelos.
Cada classe de factory implementa um método `definition()` que retorna um array associativo de atributos do modelo. A factory resolve automaticamente uma instância `FakerGenerator`, acessível via `$this->faker`, que suporta mais de 200 fornecedores de dados com reconhecimento de localidade — desde `name()` e `safeEmail()` até `iban()`, `latitude()`, `uuid()` e `creditCardNumber()`.
Capacidades principais do sistema moderno de factories:
- Encadeamento fluente de métodos para configuração de contagem, estado e relacionamentos
- Resolução lazy de atributos — as closures dentro de `definition()` são avaliadas de forma independente por instância
- Estados de factory para modelar variações específicas do domínio (ex.: contas suspensas, utilizadores verificados)
- Factories de relacionamentos que criam recursivamente modelos pai quando necessário
- Sequências para percorrer conjuntos de atributos predefinidos
- `make()` vs `create()` para instâncias em memória vs persistidas
Pré-requisitos
Antes de implementar factories, certifique-se de que o seu ambiente cumpre os seguintes requisitos:
- Laravel 9 ou posterior (o Laravel 8 é compatível, mas carece de algumas funcionalidades de sequência mais recentes)
- PHP 8.0 ou superior
- Uma ligação de base de dados configurada em `.env` (MySQL, PostgreSQL ou SQLite para testes em memória)
- O pacote `laravel/framework`, que é incluído com `fakerphp/faker` como dependência
- Modelos Eloquent com ficheiros de migração correspondentes
Para equipas que executam o Laravel em infraestrutura gerida, o VPS com cPanel fornece um ambiente conveniente para gerir tanto a stack da aplicação como os serviços de base de dados a partir de uma interface unificada.
Passo 1: Gerar uma Classe de Factory
Utilize o Artisan CLI para criar um ficheiro de factory:
“`bash
php artisan make:factory UserFactory
“`
Isto cria `database/factories/UserFactory.php`. Se pretender associar automaticamente a factory a um modelo, passe o sinalizador `–model`:
“`bash
php artisan make:factory UserFactory –model=User
“`
O Laravel resolve a ligação factory-para-modelo através de uma convenção de nomenclatura: `UserFactory` mapeia para `AppModelsUser`. Pode substituir isto definindo explicitamente a propriedade `protected $model`, o que é essencial quando os seus modelos residem fora do namespace padrão `AppModels`.
Passo 2: Definir o Blueprint da Factory
Abra `database/factories/UserFactory.php` e defina o método `definition()`:
“`php
<?php
namespace DatabaseFactories;
use AppModelsUser;
use IlluminateDatabaseEloquentFactoriesFactory;
use IlluminateSupportStr;
use IlluminateSupportFacadesHash;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => Hash::make('password'),
'remember_token' => Str::random(10),
];
}
}
“`
Notas ao nível dos atributos:
- `$this->faker->unique()->safeEmail()` — o modificador `unique()` mantém um registo de unicidade por pedido. Se esgotar os valores únicos disponíveis (raro, mas possível com conjuntos de dados muito grandes), o Faker lança uma `OverflowException`. Reponha-o com `$this->faker->unique(true)` para limpar a cache.
- `Hash::make('password')` é preferível a `bcrypt()` diretamente porque respeita o driver de hashing configurado na aplicação (bcrypt, argon2i, argon2id).
- `email_verified_at => now()` marca o utilizador como já verificado. Omita este campo ou defina-o como `null` para simular uma conta não verificada — uma variação de estado comum.
Passo 3: Criar Instâncias de Modelos
3.1 Persistir um Único Registo
“`php
$user = AppModelsUser::factory()->create();
“`
Isto executa uma instrução `INSERT` e retorna um modelo Eloquent `User` hidratado. A instância retornada reflete o estado real da base de dados, incluindo quaisquer valores padrão ou triggers ao nível da base de dados.
3.2 Persistir Múltiplos Registos
“`php
$users = AppModelsUser::factory()->count(10)->create();
“`
Retorna uma `IlluminateDatabaseEloquentCollection` de 10 instâncias `User`. Cada registo recebe valores Faker gerados de forma independente — não são cópias de uma única instância.
3.3 Instância em Memória Sem Persistência
“`php
$user = AppModelsUser::factory()->make();
“`
O método `make()` instancia o modelo e preenche os seus atributos sem interagir com a base de dados. Isto é ideal para testes unitários que verificam o comportamento do modelo, casting de atributos ou lógica de acessores/mutadores de forma isolada — mantendo os testes rápidos e independentes da base de dados.
3.4 Substituir Atributos Específicos
Tanto `create()` como `make()` aceitam um array de substituições de atributos:
“`php
$user = AppModelsUser::factory()->create([
'email' => 'specific@example.com',
'name' => 'Jane Doe',
]);
“`
As substituições têm precedência sobre os valores de `definition()`. Este é o padrão correto quando um teste depende de um valor de atributo específico e conhecido, em vez de um aleatório.
Passo 4: Estados de Factory
Os estados são modificações nomeadas à definição base da factory. Permitem modelar condições de domínio distintas sem duplicar toda a factory.
4.1 Definir Estados
“`php
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
'role' => 'administrator',
]);
}
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'suspended_at' => now(),
'is_active' => false,
]);
}
“`
4.2 Aplicar Estados
“`php
// Single state
$adminUser = AppModelsUser::factory()->admin()->create();
// Stacked states — fully composable
$suspendedAdmin = AppModelsUser::factory()->admin()->suspended()->create();
“`
Os estados são avaliados na ordem em que são encadeados. Estados posteriores sobrescrevem chaves conflituantes de estados anteriores, proporcionando uma resolução de atributos previsível e em camadas.
Passo 5: Sequências para Ciclar Valores de Atributos
Quando precisar de alternar entre um conjunto definido de valores em vez de valores aleatórios, utilize `Sequence`:
“`php
use IlluminateDatabaseEloquentFactoriesSequence;
$users = AppModelsUser::factory()
->count(6)
->state(new Sequence(
['role' => 'editor'],
['role' => 'viewer'],
['role' => 'moderator'],
))
->create();
“`
Isto percorre o array de sequência, atribuindo funções por ordem. Com 6 utilizadores, cada função é atribuída duas vezes. As sequências são inestimáveis para testar paginação, controlo de acesso baseado em funções e lógica de renderização de UI que depende de distribuições de dados variadas mas controladas.
Passo 6: Factories de Relacionamentos
6.1 Definir um Relacionamento Belongs-To
Em `PostFactory.php`, referencie a factory pai diretamente como valor de atributo:
“`php
<?php
namespace DatabaseFactories;
use AppModelsPost;
use AppModelsUser;
use IlluminateDatabaseEloquentFactoriesFactory;
class PostFactory extends Factory
{
protected $model = Post::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => $this->faker->sentence(),
'body' => $this->faker->paragraphs(3, true),
'slug' => $this->faker->unique()->slug(),
];
}
}
“`
Quando `user_id` é definido como `User::factory()`, o Laravel adia a sua avaliação. Se chamar `Post::factory()->create()` sem fornecer um `user_id`, um novo `User` é criado automaticamente e a sua chave primária é utilizada. Se fornecer um utilizador existente, a factory aninhada é completamente ignorada.
6.2 Associar a um Pai Existente
“`php
$user = AppModelsUser::factory()->create();
$posts = AppModelsPost::factory()->count(5)->for($user)->create();
“`
O método `for()` define a chave estrangeira `user_id` para a chave primária do modelo fornecido, evitando a criação desnecessária de utilizadores. Este é o padrão correto quando o seu teste já tem um utilizador específico no âmbito.
6.3 Relacionamentos Has-Many
“`php
$userWithPosts = AppModelsUser::factory()
->has(AppModelsPost::factory()->count(3), 'posts')
->create();
“`
Ou utilizando o atalho mágico `hasPosts()` (resolvido através do nome do método de relacionamento no modelo):
“`php
$userWithPosts = AppModelsUser::factory()->hasPosts(3)->create();
“`
Isto cria um utilizador e três posts associados numa única operação atómica — com todas as chaves estrangeiras resolvidas corretamente.
6.4 Relacionamentos Many-to-Many
“`php
$user = AppModelsUser::factory()
->hasAttached(
AppModelsRole::factory()->count(2),
['assigned_at' => now()]
)
->create();
“`
O método `hasAttached()` trata da inserção na tabela pivot, incluindo quaisquer atributos pivot adicionais que precise de preencher.
Passo 7: Utilizar Factories em Testes
7.1 Teste de Funcionalidade com Asserções de Base de Dados
“`php
use IlluminateFoundationTestingRefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_be_created_with_factory(): void
{
$user = AppModelsUser::factory()->create();
$this->assertDatabaseHas('users', [
'email' => $user->email,
]);
}
public function test_unverified_user_cannot_access_dashboard(): void
{
$user = AppModelsUser::factory()->unverified()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertRedirect('/email/verify');
}
}
“`
Detalhe crítico: Utilize sempre o trait `RefreshDatabase` ou `DatabaseTransactions` em classes de teste que interagem com a base de dados. `RefreshDatabase` executa migrações de raiz antes da suite de testes e envolve cada teste numa transação que é revertida posteriormente, mantendo os testes isolados e idempotentes.
7.2 Teste Unitário com `make()`
“`php
public function test_user_full_name_accessor(): void
{
$user = AppModelsUser::factory()->make([
'name' => 'Alice Wonderland',
]);
$this->assertEquals('Alice Wonderland', $user->name);
}
“`
Não ocorre qualquer interação com a base de dados. O teste é executado em microssegundos e é adequado para pipelines CI de alta frequência.
Passo 8: Seeders de Base de Dados com Factories
8.1 Criar um Seeder
“`bash
php artisan make:seeder UserSeeder
“`
8.2 Implementar o Seeder
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use IlluminateDatabaseSeeder;
class UserSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(50)
->create();
}
}
“`
8.3 Seeder Composto com Relacionamentos
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use AppModelsPost;
use IlluminateDatabaseSeeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(20)
->has(Post::factory()->count(5), 'posts')
->create();
// Create 5 admin users with no posts
User::factory()->count(5)->admin()->create();
}
}
“`
8.4 Executar o Seeder
“`bash
Run a specific seeder
php artisan db:seed –class=UserSeeder
Run all seeders defined in DatabaseSeeder
php artisan db:seed
Migrate fresh and seed in one command (common in staging resets)
php artisan migrate:fresh –seed
“`
O comando `migrate:fresh –seed` é o fluxo de trabalho padrão para repor uma base de dados de staging ou desenvolvimento para um estado conhecido e populado. Em Servidores Dedicados, este padrão é frequentemente utilizado antes de ciclos de QA para garantir um ambiente limpo e reproduzível.
Padrões Avançados e Casos Extremos
Atributos Lazy e Valores Dependentes
Os valores do Faker dentro de `definition()` são reavaliados para cada chamada de factory. No entanto, se precisar que um atributo dependa de outro, utilize uma closure:
“`php
public function definition(): array
{
$firstName = $this->faker->firstName();
$lastName = $this->faker->lastName();
return [
'first_name' => $firstName,
'last_name' => $lastName,
'email' => strtolower("{$firstName}.{$lastName}@example.com"),
'username' => strtolower("{$firstName}{$lastName}") . $this->faker->numerify('###'),
];
}
“`
Isto garante que `email` e `username` são derivados dos mesmos valores de nome, produzindo registos internamente consistentes.
Evitar a Armadilha de Overflow do `unique()`
Ao gerar grandes conjuntos de dados (10.000+ registos numa única chamada de factory), `$this->faker->unique()->safeEmail()` pode esgotar o pool de unicidade do Faker e lançar uma `OverflowException`. Mitigue isto acrescentando um UUID ou timestamp ao valor gerado:
“`php
'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',
“`
Isto garante unicidade em escala sem depender do registo interno de unicidade do Faker.
Callbacks de Factory: `afterMaking` e `afterCreating`
Utilize callbacks para executar lógica pós-criação que não pode ser expressa como um atributo simples:
“`php
public function configure(): static
{
return $this->afterCreating(function (User $user) {
$user->profile()->create([
'bio' => $this->faker->paragraph(),
'avatar' => $this->faker->imageUrl(200, 200, 'people'),
]);
});
}
“`
O método `configure()` é chamado uma vez quando a factory é instanciada. `afterCreating` é executado após o modelo ser persistido, tornando-o adequado para criar modelos relacionados que requerem a chave primária do pai.
Testar Funcionalidade de Email
Quando as factories geram utilizadores com endereços de email, os testes de integração que verificam o envio de emails beneficiam de um ambiente dedicado de Email Hosting configurado com um servidor SMTP sandbox, evitando a entrega acidental de emails de teste a endereços reais.
`create()` vs `make()` vs `makeMany()` vs `createMany()` — Comparação
| Método | Persiste na BD | Retorna | Melhor Caso de Uso |
|---|---|---|---|
| — | — | — | — |
| `create()` | Sim | Instância única do modelo | Testes de funcionalidade, seeders |
| `create(['key' => 'val'])` | Sim | Instância única do modelo | Testes que requerem valores específicos conhecidos |
| `count(n)->create()` | Sim | Coleção de n modelos | Seeding em massa, testes de paginação |
| `make()` | Não | Instância única do modelo | Testes unitários, teste de acessores/mutadores |
| `make(['key' => 'val'])` | Não | Instância única do modelo | Testes unitários rápidos com atributos controlados |
| `count(n)->make()` | Não | Coleção de n modelos | Teste de coleções em memória |
| `createMany([…])` | Sim | Coleção | Criação em lote com conjuntos de atributos distintos |
| `makeMany([…])` | Não | Coleção | Instâncias em memória em lote |
Estados de Factory vs. Substituições de Atributos — Quando Usar Cada Um
| Cenário | Abordagem Recomendada |
|---|---|
| — | — |
| Condição de domínio reutilizável (ex.: “utilizador administrador”) | Método de estado nomeado |
| Valor único específico para um teste | Substituição de atributo em `create()` |
| Ciclar por um conjunto predefinido | `Sequence` |
| Efeitos secundários pós-criação | Callback `afterCreating` |
| Valores de atributos dependentes | Atributos baseados em closures em `definition()` |
| População de relacionamentos | `has()`, `for()`, `hasAttached()` |
Lista de Verificação de Decisão Prática
Antes de escrever uma factory ou um teste que a utilize, percorra estes pontos de verificação:
- O teste depende da base de dados? Se não, utilize `make()` e evite a sobrecarga de `RefreshDatabase`.
- O teste requer um valor de atributo específico? Passe-o como substituição para `create()` — não o codifique diretamente na factory `definition()`.
- Está a testar comportamento baseado em funções? Defina estados nomeados em vez de dispersar `create(['is_admin' => true])` por múltiplos ficheiros de teste.
- Está a popular um ambiente de staging? Utilize `migrate:fresh –seed` e certifique-se de que o seu `DatabaseSeeder` compõe todos os sub-seeders na ordem de dependência correta (pais antes de filhos).
- Está a gerar mais de 5.000 registos? Evite `unique()` em campos de alta cardinalidade; utilize valores com sufixo UUID.
- Os seus modelos têm callbacks `afterCreating` que acedem a serviços externos? Simule esses serviços na configuração do seu teste ou utilize `make()` para contornar completamente o callback.
- Está a executar testes em paralelo? Utilize `DatabaseTransactions` em vez de `RefreshDatabase` para evitar conflitos de migração entre workers paralelos, ou configure ligações de base de dados separadas por worker.
Para equipas que gerem múltiplas aplicações Laravel em diferentes ambientes, os Painéis de Controlo VPS fornecem a visibilidade de infraestrutura necessária para monitorizar o desempenho da base de dados durante operações de seeding de grande escala e execuções de testes.
FAQ
Qual é a diferença entre `create()` e `make()` nas factories do Laravel?
`create()` persiste o modelo na base de dados e retorna uma instância Eloquent hidratada. `make()` constrói o modelo em memória sem qualquer interação com a base de dados. Utilize `make()` para testes unitários puros para os manter rápidos e isolados; utilize `create()` quando o teste deve verificar o estado da base de dados.
As factories do Laravel conseguem lidar com relacionamentos polimórficos?
Sim. Defina um relacionamento `morphTo` definindo as colunas morph `*_type` e `*_id` diretamente na factory `definition()`, ou utilize `afterCreating` para associar relações polimórficas após o modelo pai ser persistido. Não existe um atalho `hasMorphedByMany()` integrado, pelo que a definição explícita de atributos é a abordagem mais fiável.
Como evitar que emails gerados por factories sejam enviados durante os testes?
Defina `MAIL_MAILER=array` ou `MAIL_MAILER=log` no seu ficheiro `.env.testing`. Isto encaminha todo o correio através do driver array ou log do Laravel, capturando mensagens em memória ou escrevendo-as no ficheiro de log sem enviar para um servidor SMTP. Pode então fazer asserções sobre `Mail::assertSent()` nos seus testes.
Por que é que `faker->unique()->safeEmail()` lança uma `OverflowException` em grandes conjuntos de dados?
O modificador `unique()` do Faker mantém um registo em memória de valores previamente gerados. Quando o pool de valores únicos estruturalmente válidos se esgota — o que pode acontecer com dezenas de milhares de registos — lança `OverflowException`. A solução é acrescentar um UUID ou string aleatória ao valor de email base, garantindo unicidade sem depender do registo interno do Faker.
As factories devem ser utilizadas em seeders de produção?
As factories são concebidas para ambientes de desenvolvimento e teste. Para seeding de produção (ex.: popular tabelas de pesquisa, funções padrão ou registos de configuração), utilize classes de seeder dedicadas com valores determinísticos codificados. As factories que dependem do Faker nunca devem ser executadas contra uma base de dados de produção, pois introduzem dados imprevisíveis e não auditáveis.
