Fake all events except for model events in Laravel

Last updated 9th January, 2023

One of Laravel's most useful testing features is being able to easily disable events from firing. Doing this allows us to prevent expensive code from running when we don't need it to.

To disable all events firing in Laravel we simply have to call the fake method on the facade:

Event::fake();

// All events past this point will be "faked" and they
// won't trigger any listeners attached to them

This is great for 90% of use cases. However, there are times where you want certain events to fire, whilst faking some.

Laravel has an answer for this - in this case we can use the fakeExcept method:

Event::fakeExcept([
    \App\Events\DiscountCreated::class
]);

// As before, all events past this point will be faked
// EXCEPT for the PostPublished event, which will
// fire normally.

So good so far.

But what happens if the logic we want to leave *un-faked *is functionality that hinges off a model event listener?

Say that on save of a Discount model, we do some processing after to calculate the value this discount will provide. It's a contrived example, but bear with me.

In this case, you might have something like the following in your model:

class Discount
{
    public static function boot(): void
    {
        parent::boot();
      
        static::saving(function (Discount $discount) {
            $discount->value = static::fetchModifierFromDatabase();
        })
    }
}

Our test might depend on knowing the value of the discount, but wants to fake events that aren't required for the test to run.

We can't do what we did before with fakeExcept because we don't have the name of the class to ignore.

So what can we do?

We can replace the model event dispatcher **after **we've faked all events, and restore model events to their regular functionality. Here's how:

// Here we get the original event dispatcher out of Laravel
$initialEvent = Event::getFacadeRoot();

// Then we fake all events as we normally would...
Event::fake();

// Then we reset the event dispatcher back to the Laravel default
// *only for models*. This has the effect of disabling all event
// listeners, except for model event listeners, which we need
// to trigger for the following test
Model::setEventDispatcher($initialEvent);

This will mean that **only **your model events will run normally, all others will be faked. This can also be used in combination with fakeExcept and fakeExceptFor to run the specific events you want.

You can also only run events for specific models. All you have to do is called setEventDispatcher on the specific model you want:

Discount::setEventDispatcher($initialEvent);

// Now only the Discount model events will fire!

Maybe a little wordy, but it gets the job done. Who knows, maybe Taylor is working on a Event::fakerExceptModels feature as we speak. 😁

If you end up using this extensively, you could create a little helper function for it.

This is something I've done in a larger project, where conserving lines really helps keep things clean. This example is written with PestPHP in mind, but it should be pretty applicable to other frameworks!

// Pest.php
function fakeExceptModels(string|array $models)
{
    $initialEvent = Event::getFacadeRoot();

    Event::fake();
  
    // Loop over the desired models and reset their
    // event dispatcher to get those events firing
    // again
    foreach (Arr::wrap($models) as $model) {
        $model::setEventDispatcher($initialEvent);
    }
}

it('can fake events... except for models', function () {
    fakeExceptModels([
        Discount::class,
        User::class
    ]);
  
    // The rest of your test
});

Hope this helps πŸ€™

EMF avatar

Exactly what I was looking forπŸ‘ŒπŸ‘ , Thanks πŸ˜ƒπŸ™

DevInTheWild.

Login To Add Comments.

Want More Like This?

Subscribe to occasional updates.