How to Mock Objects in PHPUnit and Laravel

How to Mock Objects in PHPUnit and Laravel

In the process of studying and writing tests, the most difficult topic for me was the issue of creating fake objects, and therefore I often bypassed this process and rarely used all the features. But today, when I have a clear understanding of this topic, I decided to write a short article on creating Mock objects.

 

What are the Mocking?

In the process of testing our application, the code can interact with various classes and components of the system. And there are times when you don't want a class to be called during test execution. For example, because it parses some heavy file or sends a Webhook. So you can replace them with Mocking objects. And since we no longer interact with the original classes in the process of running tests, we get enough freedom in passing some parameters and returned data.

 

What are mock objects for?

So, we have roughly decided what Mock objects are. Now let's try to answer the question why we need them.

I see several important reasons for using them.

1. You have no way to use the original class. There are times when a class can send email or sms to users, send a complex API request, and we don't want tests to perform these operations.

For example, in our project there is a functionality for finding addresses that accesses the sending API to Dadata. This API is not free, each request costs money. So sending real requests in tests is a pretty bad idea. In this case, it's enough to simply create a fake dadata interaction class and tell it what data it should return to us without actually sending it to the service via API.

2. Isolated testing. The best approach to testing is to run your code in isolation. If you don't want to add too many dependencies to your testing process and keep it isolated as possible, you can't do it without mock objects.

 

An example of working with a Mock object. Testing payment gateway.

Below is an example of working with fake objects. I have created a simple application that makes a payment.

Let's imagine that we have a special payment gateway in the form of the following class:

 

<?php

namespace App\Services;

 

use App\Models\Payment;

 

class PaymentGatewayService implements PaymentGatewayServiceInterface

{

    public function doPayment(string $token, \App\Models\User $user): int

    {

        \Stripe\Stripe::setApiKey(config('services.stripe.secret'));

 

        $payment = Payment::create([

            'amount' => $user->balance,

            'currency' => 'EUR',

            'source' => $token,

            'name' => 'Balance from ' . $user->name,

            'email' => $user->email

        ]);

        return $payment->id;

    }

}

 

As you can see, the payment class uses an external dependency and in addition to creating a payment, it communicate with Stripe. And in our tests, we wouldn't want to make unnecessary API requests.

In our controller, we simply accept this service as a dependency:

 

    public function store(Request $request, PaymentGatewayService $paymentGatewayService)

    {

        $payment_id = $paymentGatewayService->doPayment($request->input('token'), Auth::user());

        return redirect(route('leads.create'))->with(['message' => 'Payment created with id: ' . $payment_id]);

    }

 

As an example, I added a simple service call here, but definitely in real projects, in addition to accepting a payment, additional business logic is added. Therefore, let's imagine that in our test we do not want to make external calls, but simply expect to receive the id of our payment. Therefore, we are left with creating a fake PaymentServiceGateway class.

Let's do that:

 

<?php

 

namespace Tests\Feature;

 

use App\Models\User;

use App\Services\PaymentGatewayService;

use Tests\TestCase;

 

class PaymentGatewayServiceTest extends TestCase

{

    public const TEST_TOKEN = 'test_token';

 

    public function testPaymentCreatedSuccessfully(): void

    {

        $this->withoutExceptionHandling();

        $paymentService = $this->mock(PaymentGatewayService::class);

        $paymentService->shouldReceive('doPayment')->with(self::TEST_TOKEN, \Mockery::type(User::class))->andReturn('payment-id');

        $response = $this->post(route('payment.store'), [

            'amount' => 2000,

            'currency' => 'EUR',

            'token' => self::TEST_TOKEN

        ]);

        $response->assertRedirect('/leads');

    }

}

 

So, we call $this->mock() method, into which we pass our class, a copy of which is to be created. Next, we tell PHPUnit that we want to send data with a test token and User class specifically for the doPayment method. And it should return the payment id to us.

Before executing the doPayment method of our service, the mocking method prepares a copy of the object of the PaymentServiceGateway class and replaces it with the original wherever it appears during execution.

 

If there are several methods in your class, for example, one accepts a payment, and the other saves it to the database, and you want to Mock only the payment acceptance method, and leave the record in the database in the original execution, you can use partial mocking:

 

$this->partialMock(PaymentGatewayService::class, function (MockInterface $mock) {

            $mock->shouldReceive('doPayment')->once();

        });

 

In this case, we will only mock the doPayment method of the PaymentGatewayService class, and for other methods it will still use the source code of the method.

 

Testing the JSON response

In case your original class returns a JSON response, you can test this behavior very easily through fake classes:

 

$this->mock(PaymentServiceGateway::class, function (MockInterface $mock) {

            $mock->shouldReceive('doPayment')

                 ->once()

                 ->andReturn(json_decode(json_encode([

                    'amount' => 5000,

                    'key' => 'test_key',

                ]))); 

        });

 

       $this->post('/payment')

            ->assertJsonFragment(['amount' => 5000]);

 

It's worth noting that you can't Mock objects of a class that is final. In this case, you need to work with interfaces:

 

        $this->mock(PaymentGatewayServiceInterface::class, function (MockInterface $mock) {

            $mock->shouldReceive('doPayment')->once();

        });

 

And controller will looks like:

 

    public function store(Request $request, PaymentGatewayServiceInterface $paymentGatewayService)

    {

        $payment_id = $paymentGatewayService->doPayment($request->input('token'), Auth::user());

        return redirect(route('leads.create'))->with(['message' => 'Payment created with id: ' . $payment_id]);

    }

 

But do not forget register it in ServiceProvider. Laravel should know, with which class it should map your interface.

For me, this is the most preferred way of working, because I most often designate classes as final so as not to create a large inheritance chain.

An important point of this testing phase is that we have not only created a separate class for accepting payment, we have minimized the connection between different components of the framework, made the code more readable and easier to refactor.

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235