El problema que ens va costar hores
Mentre implementàvem un sistema de feature flags per testejar fluxos event-driven, ens vam trobar amb un bug frustrant que ens va portar hores depurar. El nostre sistema de multi-tenancy, que funcionava perfectament en producció, de sobte fallava en tests—però només després d'introduir Event::fake().
El símptoma era clar però confús: errors de constraint de base de dades indicant tenant_id cannot be null. El camp tenant era NOT NULL a la base de dades, i el nostre trait HasTenancy havia de configurar-lo automàticament. Per què fallava de sobte?
Entenent la causa arrel
L'Event::fake() de Laravel fa exactament el que promet: reemplaça el dispatcher d'esdeveniments real amb un de fals que registra esdeveniments en lloc de despatxar-los realment. Això és perfecte per testejar que certs esdeveniments es disparen. No obstant això, hi ha un efecte secundari ocult que no és immediatament obvi.
Els models Eloquent tenen el seu propi dispatcher d'esdeveniments separat del sistema principal d'esdeveniments de l'aplicació. Quan crides Event::fake(), estàs reemplaçant el dispatcher de l'aplicació, però els models Eloquent continuen usant el seu dispatcher antic—que ara està desconnectat del fals contra el qual intentes testejar.
Això significa que les teves crides a Event::assertDispatched() funcionen (testejant esdeveniments d'aplicació), però els teus esdeveniments de model (com saving, created, etc.) deixen de funcionar completament.
Un exemple real: multi-tenancy
Aquí hi ha un trait simplificat que es va trencar silenciosament en tests:
trait HasTenancy
{
public static function bootHasTenancy()
{
static::saving(function ($model) {
$model->tenant = Tenant::current();
});
}
}Aquest trait usa l'esdeveniment saving d'Eloquent per configurar automàticament el camp tenant. En producció: funciona perfectament. En tests amb Event::fake(): el callback mai s'executa.
El test fallat
La creació del model fallava amb una violació de constraint. El camp tenant era requerit a la base de dades, però el nostre trait que havia de configurar-lo automàticament no s'estava executant. Després d'hores depurant, vam descobrir que aquest és un issue conegut de Laravel.
public function test_dispatches_events(): void
{
Event::fake(); // ← Això trenca HasTenancy!
// Això llança un error de base de dades! ✗
// SQLSTATE[23000]: Integrity constraint violation:
// tenant_id cannot be null
$model = Model::create(['name' => 'Test']);
}
La solució: reconnectar els esdeveniments de model
La solució és sorprenentment simple un cop saps que existeix. Després de cridar Event::fake(), necessites reconnectar manualment el dispatcher d'esdeveniments del model:
// Desar el dispatcher inicial abans de fer fake
$initialDispatcher = Event::getFacadeRoot();
// Fer fake d'esdeveniments per testing
Event::fake();
// Reconnectar esdeveniments de model al dispatcher fals
Model::setEventDispatcher($initialDispatcher);Aquí està el test corregit:
public function test_dispatches_events(): void
{
// Configurar Event::fake() amb la correcció del dispatcher
$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);
// ARA funciona! ✓
$model = Model::create(['name' => 'Test']);
// Tant la lògica del model COM les assercions d'esdeveniments funcionen
$this->assertNotNull($model->tenant);
Event::assertDispatched(SomeEvent::class);
}
Per què això importa
Això no és només una peculiaritat de testing—emmascara bugs seriosos. Els esdeveniments de model són crítics per a sistemes de multi-tenancy que automàticament delimiten dades, generació d'UUID en mètodes boot de models, logging d'auditoria que rastreja canvis, invalidació de caché lligada a esdeveniments de model, i actualitzacions d'índexs de cerca (Algolia, Meilisearch, etc.).
Sense reconnectar el dispatcher, estàs testejant un camí de codi diferent al de producció. Els tests passen mentre la lògica crítica s'omet.
Quan fas fake d'esdeveniments a Laravel, reconnecta sempre el dispatcher d'esdeveniments de model. El teu jo del futur t'ho agrairà.Marc BernetDesenvolupador de Software
Bones pràctiques
Reconnecta sempre en fer fake d'esdeveniments. Si estàs testejant esdeveniments i tens listeners d'esdeveniments de model, usa sempre aquest patró:
$initialDispatcher = Event::getFacadeRoot();
Event::fake();
Model::setEventDispatcher($initialDispatcher);Testeja el comportament del model, no només esdeveniments. No només facis assert d'esdeveniments. Verifica que els efectes secundaris realment van passar:
Event::assertDispatched(ModelCreated::class);
// També verifica el comportament
$model = Model::find($id);
$this->assertNotNull($model->tenant);
$this->assertEquals('active', $model->status);Considera tests d'integració. Per a fluxos complexos amb múltiples listeners, omet Event::fake() i testeja la integració completa:
public function test_full_integration(): void
{
Mail::fake(); // Segueix fent fake de serveis externs
$this->doSomething();
// Verifica l'estat final
$this->assertDatabaseHas('models', ['tenant' => 1]);
Mail::assertSent(NotificationMail::class);
}