Ralph J. Smit Laravel Software Engineer
Testing your application code is absolutely a skill you should learn. It improves your own confidence so much and it keeps your application much more maintainable in the long run. Mocking is an important part of testing you code. However, it can also be a bit hard to fully grasp the concept. In this article I'll show you all the common ways of mocking objects in Laravel.
What is mocking?
First of all, what is mocking? Mocking means that you 'fake' a class. It basically comes down to the fact that you pick a specific class and replace that with a so-called Mock.
So, what is a mock? A mock is a 'fake version' of an object. Say that you have a class that sends an email to a user whenever an order is placed. It could look a bit like this:
<?php namespace App\Actions\Order; use App\Models\Order; class SendOrderInformationAction{ public function execute(Order $order): void { $order->user->notify(/* ... */); }}
And it would be used like this:
<?php namespace App\Actions\Order; use App\DataTransferObjects\OrderDTO; class CreateOrderAction{ public function __construct( public SendOrderInformationAction $sendOrderInformationAction, ) {} public function execute(OrderDTO $order): void { // Do some stuff // Create order in database, etc. $order = /* ... */ $this->sendOrderInformationAction->execute($order); }}
Now, we want to test whether an email is indeed sent whenever an order was completed. We could do add a Notification::fake()
to the CreateOrderTest and do some Notification::assertSent(...)
stuff later.
However, that leaves us with a big downside: we are testing the behaviour of the SendOrderInformationAction
, though in fact we want to test the CreateOrderAction
class. Don't understand me wrong: it is a valid approach. But if you do this, your test suite will become quite messy and you'll almost certainly lose the overview.
The solution here is using a mock. By using a mock, we can assert that the SendOrderInformationAction
was called, but not what this other class did. We can now also create a SendOrderInformationActionTest
, and this test is going to assert whether the class is doing the right thing.
So, summarised:
-
First, we test whether the
SendOrderInformationAction
was used. -
Second, in the
SendOrderInformationActionTest
we test whether this class does the right thing.
By testing this way, you keep your testsuite clean and much more maintainable. And whenever something breaks in your testsuite, you'll immediately see where the problem is, instead of having to dug through your code.
How to mock a Laravel dependency
Let's now check out an example of how mocking looks like. Mocking in Laravel looks like this:
$this->mock(SendOrderInformationAction::class, function (MockInterface $mock) { $mock ->shouldReceive('execute') ->once() ->andReturn(true);}); app(CreateOrderAction::class)->execute(/* ... */);
There are two things here:
-
The first parameter to
$this->mock()
is the name of the class you want to mock. -
The second parameter is a closure. This closure receives the variable
$mock
. The$mock
variable can be used to specify what methods we expect te be called on the mock.
It basically means two things:
-
We're telling Mockery to construct a 'fake' object, that expects to receive the function
execute
once and returntrue
then. -
At the same time we're telling Laravel that whenever a
SendOrderInformationAction::class
is requested, it should return the fake object generated by Mockery.
Where are the Mockery assertions?
So, you might be wondering: where are the assertions? We are not defining regular assertions, like you might be used to. In fact, the tests will automatically fail if the expected function call is not recorded. So in the above example, the test will pass.
Defining Mockery expectations
The important part here is to know how to define expectations on the $mock
variable. Defining expectations is very simple and it mostly happens in a fluent way. Here are a few important functions you should know:
`$mock->shouldReceive(...)`
You may use the $mock->shouldReceive()
method to say that we expect a function to be called. In most cases you start with this function. You may define multiple expectations below each other:
$mock->shouldReceive('prepare');$mock->shouldReceive('execute');$mock->shouldReceive('cleanup');
You may use the following methods to chain after shouldReceive()
:
`->with(...)`
You may use the ->with(...)
method to define the arguments that should be given to a function. So if a function receives a boolean and an integer, you can define it like this:
$mock->shouldReceive('execute')->with(false, 8)->once(0;
You may also compare objects, but be warned that it will fail if the objects do not reference the same instance. So this will fail:
// In the test$user = User::factory()->create(); $mock ->shouldReceive('execute') ->with($user) ->once(); // The function that we're mocking:public function execute(bool $shouldBecomeSuperAdmin, User $user) { /** */ } // Somewhere else:$user = User::find($id); app(ConvertUserToSuperAdminAction::class)->execute($user); // FAIL
Why? Because the $user
object is a new object, that is retrieved again somewhere else.
Luckily there is a way to fix this: instead of directly specifying $user
, you may also specify Mockery::on(...)
as an argument. Mockery::on(...)
receives a callback. Whenever Mockery wants to compare the argument, it will give the argument to the callback. The callback should return true
or false
whether this is indeed the right argument. This is very flexible and allows us to do this:
// In the test$user = User::factory()->create(); $this->mock(ConvertUserToAdminAction::class, function (MockInterface $mock) use ($user) { $mock ->shouldReceive('execute') ->with(true, Mockery::on(function (User $argument) use ($user) { return $argument->is($user); }));}); app(ConvertUserToSuperAdminAction)->execute($user); // PASS
`->once()`
Use the ->once()
modifier to make sure that a method is only called once.
`->never()`
Use the ->never()
modifier to make sure that the method is never called.
`->andReturn(...)`
Use the ->andReturn(...)
modifier to specify the value that should be returned. This can be anything you want.
`->andThrow(...)`
You may use the ->andThrow(...)
method to define an exception that should be thrown when calling the function. This can be useful for situations where you have an Action-class that connects to an API. You should also test situations where the API is unavailable or when you are rate limited. This helper can help you with testing how errors are handled.
If you want to see all the methods you can use (and there are many), you should check out the Mockery documentation about Expectations.
Default Laravel mocks
Laravel also provides a lot of custom 'mocks' out of the box. You might not see them as mocks, because in fact they are fake implementations defined by Laravel. Nevertheless, it is important (and easy) to use them as well.
The provided mocks are:
-
Bus::fake()
for testing queues and jobs -
Event::fake()
for testing whether (or not) the right events were dispatched -
Notification::fake()
for testing whether (or not) the right notifications were sent -
Mail::fake()
for testing email -
Http::fake()
for faking HTTP-requests -
Storage::fake()
for testing the storage -
Queue::fake()
for testing jobs. In most cases you should use theBus::fake()
.
Custom mocks
If you need more fine-grained control over your mocks classes, you can also consider using a custom hand-crafted mock. This means that you create a dedicated class for your mock. Instead of specifying expectations on a $mock
, you can now write your own fake implementation. Note that this should only be done in rare cases, because it increases the complexity in your testsuite.
In order to use this, you'll need to create a new class. This class extends your base class. Add a new static function called setUp()
(or how you want it). This function should bind a new instance of the custom fake class to the container, whenever an instance of the parent class was requested.
Next you can just override all the methods (or only the ones you want) and put your custom logic in there. See the following example:
class ConvertUserToSuperAdminActionFake extends ConvertUserToSuperAdminAction{ public static function setUp(): void { app()->instance(ConvertUserToSuperAdminAction::class, new static()); } public function execute(User $user) : void { // Now you can override this function with your custom logic. }} // In our testConvertUserToSuperAdminActionFake::setUp(); // Now everywhere the ConvertUserToSuperAdminActionFake::class is used// instead of the normal ConvertUserToSuperAdminAction::class.
Always use the Laravel container
There's one final note I want to say to you, which is to always use the Laravel container, with dependency injection or service location. In the above examples you saw that I used two things to get the right class:
-
I used Laravel dependency injection in the constructor (the first example).
-
I used service location via
app(MyClass::class)
to get an instance of a class (the second example).
Use one of those techniques and Laravel will provide you with the correct instance of a class. By doing it like this, it is easy to replace a class with a mock.
If you're writing code like the following, it is almost impossible to replace the MyClass::class
with a mock:
// Don't do this!$object = new MyClass(); // But do this:$object = app(MyClass::class);
Mocks v spies
You might also have heard of the word spies when talking about mocking. A spy is similar to a mock, except that a spy doesn't fake the implementation. It only records the which functions were called, and later on you can assert if the right functions were called.
In my experience you'll use a spy less often, because you still have the problem of testing the behaviour of class B in a test for class A. Nevertheless, they also have their uses. Here's an example:
$spy = $this->spy(ConvertUserToSuperAdminAction::class); $spy ->shouldHaveReceived('execute') ->once();
Check out the Mockery docs for spies if you want to learn more.
Conclusion
As you've seen, mocking in Laravel with Mockery is not that difficult. It just requires the right tricks to learn.
Personally when I started testing, I found mocking a hard-to-grasp concept. But after a few months it gradually started to make sense to me. Now I wouldn't want to test an application without using mocks!
Hope this was useful for you! I'd encourage you to start using mocks and test your Laravel app. As always, if you have any questions or ideas, please leave a comment or let me know!
Published by Ralph J. Smit on in Laravel . Last updated on 26 March 2022 .