I always dreaded learning about Mocking in testing assuming it would be something too complex, and I never really had the requirement of using Mocking in my Laravel tests.

But now since I have a basic idea about Mocking objects, here is a simplified version of the concept in my understanding.

What is Mocking?

While testing a PHP application, the code deal with lot of classes and components. For some reason, if you don't want to interact with the original class during your test, you create a temporary replacement for your tests, let's now call them Fake / Mock classes.

We create provisions in our tests so that the Fake / Mock classes behave as the Original class and since we are not dealing with the original class, we can also leverage extra liberty of manipulating and playing around with the Fake class.

Why Mocking ?

Alright, we now understand that we can create a Fake version of out class during the tests, but why would we want to do that?

There can be multiple reasons for that.

1. Cant afford to use the Original Class : There can be a case where your original class is the one that sends and Email, SMS, Notification to the users, and we don't want to do that in our testing, and hence we want to fake a way to pretend that the application sends the notification.

In my case I was working with one External API, and I did not wanted my code to hit the API every-time I run the test, Thus I created a mock class of what return In which I assumed the API works fine and what return I expect from it and the tests focus on the code that I have written.

2. Isolation Testing : A better testing approach is to test your code in isolation. Mocking provides a way to test the code in isolation and not to be effected by the external dependencies.

Mocking Demo with a Tiny Laravel Application

With a simple example of we will see how we can achieve Object Mocking in Laravel tests.

Let's say you have a class named Context in your application


<?php

namespace App;

class Context{

    public function perform(){
        // work with databases, API's and external services
        // and complicated stuff you don't want to run
        // in a test
        return 5;
    }
}

As you can see we have a class Context which here does very little but in our live project this class is dealing with lot of things and external dependencies and thus we don't want to invoke this in our tests.

We also have a Controller in our Laravel code which uses the Context class and this has been injected into the Controller using Dependency Injection.


<?php

namespace App\Http\Controllers;

use App\Context;

class MyController extends Controller
{

    public $context;

    public function __construct(Context $context){
        $this->context = $context;
    }

    public function execute(){
        $complexMethodReturn =  $this->context->perform();
        $returnValue = $complexMethodReturn * 10;
        return $returnValue;
    }

}

execute method of the controller is dependent on the Context class, it uses its return value and does some more magic with it and then returns it.

In our test, we want to test the execute method of the controller and assert it's return value, but we don't to call the perform method of the Context class. So we will mock the context class.

Let's see how we can do that.

How to mock objects in Laravel

In our tests, to mock the Context class, we can make use of mock method that is provided by Laravel's base test case class.


        $this->mock(Context::class, function (MockInterface $mock) {
            $mock->shouldReceive('perform')->once();
        });

The first parameter of the mock method indicates the class which we are looking to Mock and the next parameter is closure function wherein we can pass additional function.

shouldReceive indicates the method that should be executed while mocking the Class and once indicates that method should be executed only once.

Here is how the complete test looks


/** @test */
    public function example_test()
    {

        $this->mock(Context::class, function (MockInterface $mock) {
            $mock->shouldReceive('perform')->once();
        });

        app(MyController::class)->execute();
    }

Before executing the execute method of the MyController class, the mock method prepares a mock object of the Context class and replaces it with the original wherever it appears in the execution.

Since this is a Controller method, if you have a route that calls on to your method, you can replace it with get request in the test.


Route::get('/my-controller-execute', [MyController::class, 'execute']);


    /** @test */
    public function example_test()
    {

        $this->mock(Context::class, function (MockInterface $mock) {
            $mock->shouldReceive('perform')->once();
        });

        $this->get('/my-controller-execute');
    }

You can also mention what the mock object should return during mocking.


....
        $this->mock(Context::class, function (MockInterface $mock) {
            $mock->shouldReceive('perform')->once()->andReturn(5);
        });

        $this->get('/my-controller-execute')
        ->assertSee(50);
...

Partial Mocking

If your class to be Mocked has multiple methods


<?php

namespace App;

class Context{

    public function perform(){
        // work with databases, API's and external services
        // and complicated stuff you don't want to run
        // in a test
        return 5;
    }

    public function fetch(){
        //Another method in class, 
        //which we are not looking to mock
        return true;
    }
}

And while mocking you are not looking to Mock all the methods you can do a partial Mock for the method you are looking to mock.


$this->partialMock(Context::class, function (MockInterface $mock) {
            $mock->shouldReceive('perform')->once();
        });

With this, we will only mock the perform method of Context class and for the other methods it will still use the original method's code.

Mocking JSON Response

If any of your original class returns JSON response, you can easily mock the that behaviour as well.


$this->mock(Context::class, function (MockInterface $mock) {
            $mock->shouldReceive('perform')
                 ->once()
                 ->andReturn(json_decode(json_encode([
                    'name' => 'Tushar',
                    'email' => 'tushar@5balloons.info',
                ]))); 
        });

       $this->get('/test-route')
            ->assertJsonFragment(['name' => 'Tushar']);
Comments