Laravel IoC, mocking, tests - practical example

Published by Dev Kordeš on 10/30/2019

Practical example on how to use Laravel's IoC container to mock external API call in our unit tests. This post shows how to set up our classes for easy testability and then mock then in our unit tests.

Laravel's IoC is a powerful tool for managing our app's dependencies. From the (4.2) official documentation :

The Laravel inversion of control container is a powerful tool for managing class dependencies. Dependency injection is a method of removing hard-coded class dependencies. Instead, the dependencies are injected at run-time, allowing for greater flexibility as dependency implementations may be swapped easily.

This blog touches on how swapping the implementation is useful when we're writing automated tests.

But enough about theory...

Example

In this example, I'll set up:

1.) a class that fetches data from Open Exchange Rates

2.) a class that parses the response and converts currency A to currency B

3.) a unit test that mocks the fetching part

Fetch the data

First, we need a class that fetches the Open Exchange Rate API for currency exchange rates - and converts the response to an array.

Shoutouts to Freek and his post on actions , which is how I'll design the 2 classes.

The GetExchangeRates class fetches the data from the API using Guzzle :

      <?php

namespace App\Actions;

use GuzzleHttp\{
    RequestOptions,
    ClientInterface,
};

class GetExchangeRates
{
    protected $client;
    protected $appId;

    public function __construct(ClientInterface $client, string $appId)
    {
        $this->client = $client;
        $this->appId = $appId;
    }

    public function execute(string $baseCurrency = 'USD')
    {
        return json_decode(
            $this
                ->client
                ->request(
                    'GET', 
                    'latest.json', 
                    [
                        RequestOptions::QUERY => [
                            'base' => $baseCurrency,
                            'app_id' => $this->appId,
                        ],
                    ]
                )
                ->getBody(),
            true
        );
    }
}
    

For the sake of simplicity of the blog post, it also converts the response into an array using json_decode(response body, true).

Check it out on github: https://github.com/d1am0nd/laravel-example/blob/master/app/Actions/GetExchangeRates.php

Dependencies, dependencies

As you can see, the class has 2 dependencies - the ClientInterface and the appId. We want Laravel's IoC container to resolve those 2 dependencies automatically every time we instantiate our GetExchangeRates class through the IoC.

To do that, we need to bind the dependencies in a service provider:

      <?php

namespace App\Providers;

use GuzzleHttp\{
    Client,
    RequestOptions,
};
use App\Actions\GetExchangeRates;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(GetExchangeRates::class, function ($app) {
            return new GetExchangeRates(
                new Client([
                    'base_uri' => 'https://openexchangerates.org/api/',
                ]),
                'SOME_APP_ID' // Should pull from config which should pull from ENV
            );
        });
    }
}
    

Here I've told Laravel that whenever we instantiate GetExchangeRates through it's IoC container, pass in the Guzzle client as the first parameter and 'SOME_APP_ID' as the 2nd parameter.

This means that every time we resolve GetExchangeRates through IoC container, the constructor dependencies will be automatically passed in.

Check it out on github: https://github.com/d1am0nd/laravel-example/blob/master/app/Providers/AppServiceProvider.php

Test it

We can now do this inside our routes file:

      Route::get('/', function () {
    // Instantiate through IoC and call the `execute()` method
    return app(\App\Actions\GetExchangeRates::class)->execute();
});
    

Provided that we've replaced SOME_APP_ID with an actual, valid Open Exchange Rates app id, we should be able to visit our app's / and get the actual result of the API call:

      {
  "disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
  "license": "https://openexchangerates.org/license",
  "timestamp": 1572408000,
  "base": "USD",
  "rates": {
    "AED": 3.6729,
    "AFN": 78.25001
    // other currency rates
  }
}
    

Not bad!

Convert currency

Next, we'll make a new class that can execute the above and convert amount N of currency A to currency B:

      <?php

namespace App\Actions;

use App\Actions\GetExchangeRates;

class ExchangeCurrency
{
    protected $getExchangeRates;

    public function __construct(GetExchangeRates $getExchangeRates)
    {
        $this->getExchangeRates = $getExchangeRates;
    }

    public function execute(string $fromCurrency = 'USD', $toCurrency = 'EUR', int $amount = 1)
    {
        return $this
            ->getExchangeRates
            ->execute($fromCurrency)['rates'][$toCurrency] * $amount;
    }
}
    

This class basically just uses GetExchangeRates to fetch the rates and then parses the response to get the rate from $fromCurrency to $toCurrency and multiplies it by $amount. The execute(..) method should return a floating number.

Again, the dependency (GetExchangeRates) is passed through the constructor. But this time we don't need to add anything new to AppServiceProvider, because Laravel's IoC container is already aware of how to instantiate the GetExchangeClass - which is this classes only dependency. Laravel will magically know what to pass in whenever we instantiate the class through the IoC.

Check it out on github: https://github.com/d1am0nd/laravel-example/blob/master/app/Actions/ExchangeCurrency.php

We can try doing this in our routes:

      Route::get('/', function () {
    // Instantiate through IoC and call the `execute()` method
    return app(\App\Actions\ExchangeCurrency::class)->execute('USD', 'EUR', 5);
});
    

And if we visit the page, we should get a number, something like (at the time of my testing): 4.501

Which tells us that $5 equals to 4.501€.

Great!

Unit tests & mocking

Now to create a unit test we can use the artisan command:

      php artisan make:test ExchangeCurrencyTest --unit
    

And the actual test:

      
<?php

namespace Tests\Unit;

use App\Actions\{
    ExchangeCurrency,
    GetExchangeRates,
};
use Tests\TestCase;

class ExchangeCurrencyTest extends TestCase
{
    // Store the mocked exchange rate for easy reuse
    protected $usdToEur = 0.8;

    /** @test */
    function it_successfully_converts_currency()
    {
        $this->mock(GetExchangeRates::class, function ($mock) {
            $mock
                // Assert that GetExchangeRate's `execute(..)` method is called
                ->shouldReceive('execute')
                // with argument 'USD'
                ->with('USD')
                // And return this (instead of actally executing it)
                ->andReturn([
                    'base' => 'USD',
                    'rates' => [
                        'EUR' => $this->usdToEur,
                    ],
                ]);
        });

        // Instantiate through IoC and call execute 
        $converted = app(ExchangeCurrency::class)->execute('USD', 'EUR', $amount = 3);

        // Assert that the returned amount equals to $amount * (our mocked rate)
        $this->assertEquals(
            $this->usdToEur * $amount,
            $converted
        );
    }
}
    

Explanation:

The $this->mock(someClass, ..) part tells Laravel that whenever we are instantiating someClass through IoC, it should instead resolve to an instance of Mockery ( more in Laravel docs ).

It then passes the Mockery instance to the closure in 2nd argument. In the closure, we then need to make our assertions - that is, telling the IoC what method we expect to be called (shouldReceive('methodName')) on someClass and with what arguments (with(args)). We can also specify what the method should return (andReturn(returnValue)).

In the ->andReturn(..) part I've specified an array that resembles what the actual OpenExchangeRates API returns. For the sake of readability, I've only specified the currencies we are testing.

In this example, the test would fail if ->execute(..) isn't called on the GetExchangeRates class or if it receives any argument other than 'USD'.

The actual Mockery object offers many more assertions and helpful methods. But these are the basics and we can always refer to the official documentation for more.

Next, we instantiate the class we're testing through IoC and call the ->execute(..) method with some arguments:

      $converted = app(ExchangeCurrency::class)->execute('USD', 'EUR', $amount = 3);
    

And as the last thing, we're testing that the method successfully returns converted amount:

      $this->assertEquals(
    $this->usdToEur * $amount,
    $converted
);
    

And it does! Running

      ./vendor/bin/phpunit tests/Unit/ExchangeCurrencyTest.php
    

passes successfully!

Here's the full test: https://github.com/d1am0nd/laravel-example/blob/master/tests/Unit/ExchangeCurrencyTest.php

Happy testing!

This website uses  Google Analytics  cookies. Beware.