El problema que nos costó horas
Mientras implementábamos un sistema de feature flags para testear flujos event-driven, nos encontramos con un bug frustrante que nos llevó horas depurar. Nuestro sistema de multi-tenancy, que funcionaba perfectamente en producción, de repente fallaba en tests—pero solo después de introducir Event::fake().
El síntoma era claro pero confuso: errores de constraint de base de datos indicando tenant_id cannot be null. El campo tenant era NOT NULL en la base de datos, y nuestro trait HasTenancy debía configurarlo automáticamente. ¿Por qué fallaba de repente?
Entendiendo la causa raíz
El Event::fake() de Laravel hace exactamente lo que promete: reemplaza el dispatcher de eventos real con uno falso que registra eventos en lugar de despacharlos realmente. Esto es perfecto para testear que ciertos eventos se disparan. Sin embargo, hay un efecto secundario oculto que no es inmediatamente obvio.
Los modelos Eloquent tienen su propio dispatcher de eventos separado del sistema principal de eventos de la aplicación. Cuando llamas a Event::fake(), estás reemplazando el dispatcher de la aplicación, pero los modelos Eloquent continúan usando su dispatcher antiguo—que ahora está desconectado del falso contra el que intentas testear.
Esto significa que tus llamadas a Event::assertDispatched() funcionan (testeando eventos de aplicación), pero tus eventos de modelo (como saving, created, etc.) dejan de funcionar por completo.
Un ejemplo real: multi-tenancy
Aquí hay un trait simplificado que se rompió silenciosamente en tests:
trait HasTenancy
{
public static function bootHasTenancy()
{
static::saving(function ($model) {
$model->tenant = Tenant::current();
});
}
}Este trait usa el evento saving de Eloquent para configurar automáticamente el campo tenant. En producción: funciona perfectamente. En tests con Event::fake(): el callback nunca se ejecuta.
El test fallido
La creación del modelo fallaba con una violación de constraint. El campo tenant era requerido en la base de datos, pero nuestro trait que debía configurarlo automáticamente no se estaba ejecutando. Después de horas depurando, descubrimos que este es un issue conocido de Laravel.
public function test_dispatches_events(): void
{
Event::fake(); // ← ¡Esto rompe HasTenancy!
// ¡Esto lanza un error de base de datos! ✗
// SQLSTATE[23000]: Integrity constraint violation:
// tenant_id cannot be null
$model = Model::create(['name' => 'Test']);
}
La solución: reconectar los eventos de modelo
La solución es sorprendentemente simple una vez que sabes que existe. Después de llamar a Event::fake(), necesitas reconectar manualmente el dispatcher de eventos del modelo:
// Guardar el dispatcher inicial antes de hacer fake
$initialDispatcher = Event::getFacadeRoot();
// Hacer fake de eventos para testing
Event::fake();
// Reconectar eventos de modelo al dispatcher falso
Model::setEventDispatcher($initialDispatcher);Aquí está el test corregido:
public function test_dispatches_events(): void
{
// Configurar Event::fake() con la corrección del dispatcher
$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);
// ¡AHORA funciona! ✓
$model = Model::create(['name' => 'Test']);
// Tanto la lógica del modelo COMO las aserciones de eventos funcionan
$this->assertNotNull($model->tenant);
Event::assertDispatched(SomeEvent::class);
}
Por qué esto importa
Esto no es solo una peculiaridad de testing—enmascara bugs serios. Los eventos de modelo son críticos para sistemas de multi-tenancy que automáticamente delimitan datos, generación de UUID en métodos boot de modelos, logging de auditoría que rastrea cambios, invalidación de caché ligada a eventos de modelo, y actualizaciones de índices de búsqueda (Algolia, Meilisearch, etc.).
Sin reconectar el dispatcher, estás testeando un camino de código diferente al de producción. Los tests pasan mientras la lógica crítica se omite.
Cuando haces fake de eventos en Laravel, reconecta siempre el dispatcher de eventos de modelo. Tu yo del futuro te lo agradecerá.Marc BernetDesarrollador de Software
Buenas prácticas
Reconecta siempre al hacer fake de eventos. Si estás testeando eventos y tienes listeners de eventos de modelo, usa siempre este patrón:
$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);Testea el comportamiento del modelo, no solo eventos. No solo hagas assert de eventos. Verifica que los efectos secundarios realmente ocurrieron:
Event::assertDispatched(ModelCreated::class);
// También verifica el comportamiento
$model = Model::find($id);
$this->assertNotNull($model->tenant);
$this->assertEquals('active', $model->status);Considera tests de integración. Para flujos complejos con múltiples listeners, omite Event::fake() y testea la integración completa:
public function test_full_integration(): void
{
Mail::fake(); // Sigue haciendo fake de servicios externos
$this->doSomething();
// Verifica el estado final
$this->assertDatabaseHas('models', ['tenant' => 1]);
Mail::assertSent(NotificationMail::class);
}