15%

Ahorra 15%<\/span> en todos los servicios de hosting

Pon a prueba tus habilidades y obtén Descuento<\/span> en cualquier plan de hosting

Usa el código:

Skills
Comenzar
09.10.2024

Fábricas de Laravel: Construyendo Datos de Prueba Realistas con Modelos de Fábrica de Laravel

Al desarrollar aplicaciones con Laravel, uno de los cuellos de botella más comunes en el flujo de trabajo de pruebas es generar datos significativos y realistas. Las factories de Laravel son clases que definen un esquema para crear instancias de modelos Eloquent, utilizando la biblioteca PHP Faker para producir valores de atributos aleatorios pero estructuralmente válidos, lo que permite a los desarrolladores poblar bases de datos y escribir pruebas aisladas sin construir manualmente fixtures de datos.

A diferencia de los archivos SQL estáticos o los arrays codificados de forma fija, las factories son componibles, con estado y conscientes de las relaciones. Se integran directamente con las suites de pruebas PHPUnit y Pest, admiten evaluación diferida de atributos y escalan desde una única instancia de modelo hasta miles de registros en una sola cadena de métodos. Si está ejecutando Laravel en un entorno de VPS Hosting, las factories se vuelven especialmente valiosas durante las ejecuciones de pipelines CI/CD, los restablecimientos de entornos de staging y los escenarios de pruebas de carga donde la generación de datos repetible y controlada es imprescindible.

Qué son las factories de Laravel y por qué son importantes

Las factories de Laravel fueron rediseñadas fundamentalmente en Laravel 8. El antiguo enfoque basado en closures `$factory->define()` fue reemplazado por clases PHP dedicadas que extienden `IlluminateDatabaseEloquentFactoriesFactory`. Este cambio arquitectónico introdujo seguridad de tipos, autocompletado en IDE y una separación más clara entre la lógica de factory y las definiciones de modelos.

Cada clase factory implementa un método `definition()` que devuelve un array asociativo de atributos del modelo. La factory resuelve una instancia de `FakerGenerator` automáticamente, accesible a través de `$this->faker`, que admite más de 200 proveedores de datos con reconocimiento de idioma, desde `name()` y `safeEmail()` hasta `iban()`, `latitude()`, `uuid()` y `creditCardNumber()`.

Capacidades clave del sistema moderno de factories:

  • Encadenamiento fluido de métodos para configuración de count, state y relaciones
  • Resolución diferida de atributos: los closures dentro de `definition()` se evalúan de forma nueva por instancia
  • Estados de factory para modelar variaciones específicas del dominio (p. ej., cuentas suspendidas, usuarios verificados)
  • Factories de relaciones que crean recursivamente modelos padre cuando es necesario
  • Secuencias para iterar a través de conjuntos de atributos predefinidos
  • `make()` vs `create()` para instancias en memoria vs persistidas

Requisitos previos

Antes de implementar factories, asegúrese de que su entorno cumple los siguientes requisitos:

  • Laravel 9 o posterior (Laravel 8 es compatible pero carece de algunas funciones de secuencia más nuevas)
  • PHP 8.0 o superior
  • Una conexión de base de datos configurada en `.env` (MySQL, PostgreSQL o SQLite para pruebas en memoria)
  • El paquete `laravel/framework`, que se incluye con `fakerphp/faker` como dependencia
  • Modelos Eloquent con los archivos de migración correspondientes

Para equipos que ejecutan Laravel en infraestructura gestionada, VPS con cPanel proporciona un entorno conveniente para gestionar tanto la pila de aplicaciones como los servicios de base de datos desde una interfaz unificada.

Paso 1: Generar una clase factory

Use el CLI de Artisan para crear un archivo factory:

“`bash

php artisan make:factory UserFactory

“`

Esto crea `database/factories/UserFactory.php`. Si desea asociar automáticamente la factory con un modelo, pase el indicador `–model`:

“`bash

php artisan make:factory UserFactory –model=User

“`

Laravel resuelve el enlace factory-modelo a través de una convención de nomenclatura: `UserFactory` se asigna a `AppModelsUser`. Puede anular esto estableciendo la propiedad `protected $model` explícitamente, lo cual es esencial cuando sus modelos se encuentran fuera del espacio de nombres predeterminado `AppModels`.

Paso 2: Definir el esquema de la factory

Abra `database/factories/UserFactory.php` y defina el 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 a nivel de atributo:

  • `$this->faker->unique()->safeEmail()`: el modificador `unique()` mantiene un registro de unicidad por solicitud. Si agota los valores únicos disponibles (raro pero posible con conjuntos de datos muy grandes), Faker lanza una excepción `OverflowException`. Restablézcalo con `$this->faker->unique(true)` para limpiar la caché.
  • `Hash::make('password')` es preferible a `bcrypt()` directamente porque respeta el driver de hashing configurado de la aplicación (bcrypt, argon2i, argon2id).
  • `email_verified_at => now()` marca al usuario como ya verificado. Omita este campo o establézcalo en `null` para simular una cuenta no verificada, una variación de estado común.

Paso 3: Crear instancias de modelos

3.1 Persistir un único registro

“`php

$user = AppModelsUser::factory()->create();

“`

Esto ejecuta una sentencia `INSERT` y devuelve un modelo Eloquent `User` hidratado. La instancia devuelta refleja el estado real de la base de datos, incluidos los valores predeterminados a nivel de base de datos o los triggers.

3.2 Persistir múltiples registros

“`php

$users = AppModelsUser::factory()->count(10)->create();

“`

Devuelve una `IlluminateDatabaseEloquentCollection` de 10 instancias de `User`. Cada registro recibe valores Faker generados de forma independiente; no son copias de una única instancia.

3.3 Instancia en memoria sin persistencia

“`php

$user = AppModelsUser::factory()->make();

“`

El método `make()` instancia el modelo y rellena sus atributos sin tocar la base de datos. Esto es ideal para pruebas unitarias que verifican el comportamiento del modelo, la conversión de atributos o la lógica de accesores/mutadores de forma aislada, manteniendo las pruebas rápidas e independientes de la base de datos.

3.4 Sobreescribir atributos específicos

Tanto `create()` como `make()` aceptan un array de sobreescrituras de atributos:

“`php

$user = AppModelsUser::factory()->create([

'email' => 'specific@example.com',

'name' => 'Jane Doe',

]);

“`

Las sobreescrituras tienen prioridad sobre los valores de `definition()`. Este es el patrón correcto cuando una prueba depende de un valor de atributo específico y conocido en lugar de uno aleatorio.

Paso 4: Estados de factory

Los estados son modificaciones con nombre a la definición base de la factory. Permiten modelar condiciones de dominio distintas sin duplicar toda la 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();

“`

Los estados se evalúan en el orden en que se encadenan. Los estados posteriores sobrescriben las claves en conflicto de los anteriores, lo que le proporciona una resolución de atributos predecible y en capas.

Paso 5: Secuencias para iterar valores de atributos

Cuando necesite alternar entre un conjunto definido de valores en lugar de valores aleatorios, use `Sequence`:

“`php

use IlluminateDatabaseEloquentFactoriesSequence;

$users = AppModelsUser::factory()

->count(6)

->state(new Sequence(

['role' => 'editor'],

['role' => 'viewer'],

['role' => 'moderator'],

))

->create();

“`

Esto itera a través del array de secuencia, asignando roles en orden. Con 6 usuarios, cada rol se asigna dos veces. Las secuencias son invaluables para probar la paginación, el control de acceso basado en roles y la lógica de renderizado de UI que depende de distribuciones de datos variadas pero controladas.

Paso 6: Factories de relaciones

6.1 Definir una relación Belongs-To

En `PostFactory.php`, haga referencia a la factory padre directamente 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(),

];

}

}

“`

Cuando `user_id` se establece en `User::factory()`, Laravel difiere su evaluación. Si llama a `Post::factory()->create()` sin proporcionar un `user_id`, se crea automáticamente un nuevo `User` y se utiliza su clave primaria. Si proporciona un usuario existente, la factory anidada se omite por completo.

6.2 Adjuntar a un padre existente

“`php

$user = AppModelsUser::factory()->create();

$posts = AppModelsPost::factory()->count(5)->for($user)->create();

“`

El método `for()` establece la clave foránea `user_id` en la clave primaria del modelo proporcionado, evitando la creación innecesaria de usuarios. Este es el patrón correcto cuando su prueba ya tiene un usuario específico en el ámbito.

6.3 Relaciones Has-Many

“`php

$userWithPosts = AppModelsUser::factory()

->has(AppModelsPost::factory()->count(3), 'posts')

->create();

“`

O usando el atajo mágico `hasPosts()` (resuelto a través del nombre del método de relación en el modelo):

“`php

$userWithPosts = AppModelsUser::factory()->hasPosts(3)->create();

“`

Esto crea un usuario y tres publicaciones asociadas en una única operación atómica, con todas las claves foráneas resueltas correctamente.

6.4 Relaciones Many-to-Many

“`php

$user = AppModelsUser::factory()

->hasAttached(

AppModelsRole::factory()->count(2),

['assigned_at' => now()]

)

->create();

“`

El método `hasAttached()` gestiona la inserción en la tabla pivot, incluidos los atributos pivot adicionales que necesite rellenar.

Paso 7: Usar factories en pruebas

7.1 Prueba de funcionalidad con aserciones de base de datos

“`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');

}

}

“`

Detalle crítico: Utilice siempre el trait `RefreshDatabase` o `DatabaseTransactions` en las clases de prueba que interactúan con la base de datos. `RefreshDatabase` ejecuta las migraciones desde cero antes de la suite de pruebas y envuelve cada prueba en una transacción que se revierte posteriormente, manteniendo las pruebas aisladas e idempotentes.

7.2 Prueba unitaria con `make()`

“`php

public function test_user_full_name_accessor(): void

{

$user = AppModelsUser::factory()->make([

'name' => 'Alice Wonderland',

]);

$this->assertEquals('Alice Wonderland', $user->name);

}

“`

No se produce ninguna interacción con la base de datos. La prueba se ejecuta en microsegundos y es adecuada para pipelines CI de alta frecuencia.

Paso 8: Seeders de base de datos con factories

8.1 Crear un seeder

“`bash

php artisan make:seeder UserSeeder

“`

8.2 Implementar el 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 compuesto con relaciones

“`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 Ejecutar el 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

“`

El comando `migrate:fresh –seed` es el flujo de trabajo estándar para restablecer una base de datos de staging o desarrollo a un estado conocido y poblado. En los Servidores Dedicados, este patrón se utiliza frecuentemente antes de los ciclos de QA para garantizar un entorno limpio y reproducible.

Patrones avanzados y casos límite

Atributos diferidos y valores dependientes

Los valores de Faker dentro de `definition()` se reevalúan para cada llamada a la factory. Sin embargo, si necesita que un atributo dependa de otro, use un 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('###'),

];

}

“`

Esto garantiza que `email` y `username` se deriven de los mismos valores de nombre, produciendo registros internamente consistentes.

Evitar el desbordamiento de `unique()`

Al generar grandes conjuntos de datos (más de 10.000 registros en una sola llamada a la factory), `$this->faker->unique()->safeEmail()` puede agotar el pool de unicidad de Faker y lanzar una excepción `OverflowException`. Mitigue esto añadiendo un UUID o timestamp al valor generado:

“`php

'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',

“`

Esto garantiza la unicidad a escala sin depender del registro de unicidad interno de Faker.

Callbacks de factory: `afterMaking` y `afterCreating`

Use callbacks para realizar lógica posterior a la creación que no puede expresarse como un atributo simple:

“`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'),

]);

});

}

“`

El método `configure()` se llama una vez cuando se instancia la factory. `afterCreating` se ejecuta después de que el modelo se persiste, lo que lo hace adecuado para crear modelos relacionados que requieren la clave primaria del padre.

Pruebas de funcionalidad de correo electrónico

Cuando las factories generan usuarios con direcciones de correo electrónico, las pruebas de integración que verifican el envío de correos se benefician de un entorno de Email Hosting dedicado configurado con un servidor SMTP de sandbox, lo que evita la entrega accidental de correos de prueba a direcciones reales.

`create()` vs `make()` vs `makeMany()` vs `createMany()` — Comparación

MétodoPersiste en BDDevuelveMejor caso de uso
`create()`Instancia única del modeloPruebas de funcionalidad, seeders
`create(['key' => 'val'])`Instancia única del modeloPruebas que requieren valores conocidos específicos
`count(n)->create()`Colección de n modelosSeeding masivo, pruebas de paginación
`make()`NoInstancia única del modeloPruebas unitarias, pruebas de accesores/mutadores
`make(['key' => 'val'])`NoInstancia única del modeloPruebas unitarias rápidas con atributos controlados
`count(n)->make()`NoColección de n modelosPruebas de colecciones en memoria
`createMany([…])`ColecciónCreación por lotes con conjuntos de atributos distintos
`makeMany([…])`NoColecciónInstancias en memoria por lotes

Estados de factory vs. sobreescrituras de atributos — Cuándo usar cada uno

EscenarioEnfoque recomendado
Condición de dominio reutilizable (p. ej., “usuario administrador”)Método de estado con nombre
Valor puntual específico de la pruebaSobreescritura de atributo en `create()`
Iterar a través de un conjunto predefinido`Sequence`
Efectos secundarios posteriores a la creaciónCallback `afterCreating`
Valores de atributos dependientesAtributos basados en closures en `definition()`
Población de relaciones`has()`, `for()`, `hasAttached()`

Lista de verificación práctica

Antes de escribir una factory o una prueba que la utilice, revise estos puntos:

  • ¿La prueba depende de la base de datos? Si no es así, use `make()` y evite la sobrecarga de `RefreshDatabase`.
  • ¿La prueba requiere un valor de atributo específico? Páselo como sobreescritura a `create()`; no lo codifique de forma fija en la factory `definition()`.
  • ¿Está probando comportamiento basado en roles? Defina estados con nombre en lugar de dispersar `create(['is_admin' => true])` por múltiples archivos de prueba.
  • ¿Está poblando un entorno de staging? Use `migrate:fresh –seed` y asegúrese de que su `DatabaseSeeder` componga todos los sub-seeders en el orden de dependencia correcto (padres antes que hijos).
  • ¿Está generando más de 5.000 registros? Evite `unique()` en campos de alta cardinalidad; use valores con sufijo UUID en su lugar.
  • ¿Sus modelos tienen callbacks `afterCreating` que acceden a servicios externos? Simule esos servicios en la configuración de su prueba o use `make()` para omitir el callback por completo.
  • ¿Está ejecutando pruebas en paralelo? Use `DatabaseTransactions` en lugar de `RefreshDatabase` para evitar conflictos de migración entre workers paralelos, o configure conexiones de base de datos separadas por worker.

Para equipos que gestionan múltiples aplicaciones Laravel en distintos entornos, los Paneles de Control VPS proporcionan la visibilidad de infraestructura necesaria para monitorear el rendimiento de la base de datos durante operaciones de seeding masivo y ejecuciones de pruebas.

Preguntas frecuentes

¿Cuál es la diferencia entre `create()` y `make()` en las factories de Laravel?

`create()` persiste el modelo en la base de datos y devuelve una instancia Eloquent hidratada. `make()` construye el modelo en memoria sin ninguna interacción con la base de datos. Use `make()` para pruebas unitarias puras para mantenerlas rápidas y aisladas; use `create()` cuando la prueba deba verificar el estado de la base de datos.

¿Pueden las factories de Laravel manejar relaciones polimórficas?

Sí. Defina una relación `morphTo` estableciendo las columnas morph `*_type` y `*_id` directamente en la factory `definition()`, o use `afterCreating` para adjuntar relaciones polimórficas después de que el modelo padre sea persistido. No existe un atajo integrado `hasMorphedByMany()`, por lo que la configuración explícita de atributos es el enfoque más fiable.

¿Cómo se evita que los correos electrónicos generados por factories se envíen durante las pruebas?

Establezca `MAIL_MAILER=array` o `MAIL_MAILER=log` en su archivo `.env.testing`. Esto enruta todo el correo a través del driver de array o log de Laravel, capturando los mensajes en memoria o escribiéndolos en el archivo de log sin enviarlos a un servidor SMTP. Luego puede hacer aserciones sobre `Mail::assertSent()` en sus pruebas.

¿Por qué `faker->unique()->safeEmail()` lanza una excepción `OverflowException` en conjuntos de datos grandes?

El modificador `unique()` de Faker mantiene un registro en memoria de los valores generados anteriormente. Cuando se agota el pool de valores únicos estructuralmente válidos, lo que puede ocurrir con decenas de miles de registros, lanza `OverflowException`. La solución es añadir un UUID o cadena aleatoria al valor de correo electrónico base, garantizando la unicidad sin depender del registro de Faker.

¿Deben usarse las factories en seeders de producción?

Las factories están diseñadas para entornos de desarrollo y pruebas. Para el seeding en producción (p. ej., rellenar tablas de referencia, roles predeterminados o registros de configuración), use clases seeder dedicadas con valores deterministas codificados de forma fija. Las factories que dependen de Faker nunca deben ejecutarse contra una base de datos de producción, ya que introducen datos impredecibles y no auditables.

15%

Ahorra 15%<\/span> en todos los servicios de hosting

Pon a prueba tus habilidades y obtén Descuento<\/span> en cualquier plan de hosting

Usa el código:

Skills
Comenzar