Using Laravel events in unit tests

  • (Unit)Tests are slower when the framework needs to boot. (This becomes a problem when your application grows in size)
  • Magic often can’t be recognized by an IDE. For example, can you find all classes that dispatch events in your application?

Dispatching Events within Laravel

UserCreated::dispatch($user->id);
event(new UserCreated($user->id));
trait Dispatchable
{
/**
* Dispatch the event with the given arguments.
*
*
@return void
*/
public static function
dispatch()
{
return event(new static(...func_get_args()));
}
}
// Given this simple event:
class UserCreated(public readonly string $userId)
{
}
new UserCreated(); //IDE will give a warning that one parameter is expectednew UserCreated(1); //IDE will give a warning that a string is expectedUserCreated::dispatch(); // IDE doesn't give a warning here
UserCreated::dispatch(1); // IDE doesn't give a warning here
function event(...$args)
{
return app('events')->dispatch(...$args);
}

Laravel’s Event testing helpers

Event::fake(); // fake the event busEvent::assertDispatched(UserCreated::class); // assert that the event was dispatched

Dispatching events without helper methods

use Illuminate\Contracts\Events\Dispatcher;

class RegisterUser
{
public function __construct(
protected Dispatcher $dispatcher
) {
}

public function execute(string $userId)
{
// Make a call to some repository here
$this->dispatcher->dispatch(new UserRegistered($userId));
}
}
class UserController
{
public function __invoke(Request $request, RegisterUser $registerUser)
{
$registerUser->execute($request->input('userId'));
return response()->json(['success' => true]);
}
}

Refactor our test

class RegisterUserTest extends \Tests\TestCase
{
/** @test */
public function
it_dispatches_an_event_when_a_user_is_registered()
{
Event::fake();

$action = resolve(RegisterUser::class);
$action->execute('testUserId');

Event::assertDispatched(UserRegistered::class, function (UserRegistered $event){
$this->assertEquals('testUserId', $event->userId);
return true;
});
}
}
use Illuminate\Events\Dispatcher;
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Testing\Fakes\EventFake;

class RegisterUserTest extends \PHPUnit\Framework\TestCase
{
/** @test */
public function
it_dispatches_a_event_when_a_user_is_registered()
{
$fakeDispatcher = new EventFake(new NullDispatcher(new Dispatcher()));
$action = new RegisterUser($fakeDispatcher);
$action->execute('testUserId');

$fakeDispatcher->assertDispatched(UserRegistered::class, function (UserRegistered $event){
$this->assertEquals('testUserId', $event->userId);
return true;
});
}
}
use Illuminate\Events\NullDispatcher;
use Illuminate\Support\Testing\Fakes\EventFake;

trait InteractsWithEvents
{
private EventFake $dispatcher;

public function eventDispatcher(): EventFake
{
if (!isset($this->dispatcher)) {
$this->dispatcher = new EventFake(new NullDispatcher(new \Illuminate\Events\Dispatcher()));
}
return $this->dispatcher;
}
}
class RegisterUserTest extends \PHPUnit\Framework\TestCase
{
use InteractsWithEvents;

/** @test */
public function
it_dispatches_a_event_when_a_user_is_registered()
{
$action = new RegisterUser($this->eventDispatcher());
$action->execute('testUserId');

$this->eventDispatcher()->assertDispatched(UserRegistered::class, function (UserRegistered $event){
$this->assertEquals('testUserId', $event->userId);
return true;
});
}
}

Conclusion

  1. Unit tests: this method of event dispatching allows us to run unit tests without booting the framework. On my machine, a simple unit test when booting Laravel takes +- 60ms, whereas a test without booting the framework takes +-12ms.
    For one test, this isn’t a big difference, but do the math when you’d run more than thousands of unit tests.
  2. IDE support: Since every class that dispatches events now has the Dispatcher contract as a dependency, we can ask the IDE “Show us all classes that depend on the dispatcher”.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store