11 Apr 2023

Test doubles in PHP - Mocking

Mocking is used to test code that depends on external services that may be unreliable or difficult to test directly, such as third-party APIs.

By simulating the behavior of these services using mock objects, we ensure that the code works as it should be without the need to call the real API.

Consider utilizing Google Vision in your project. If you intend to test your code dependent on Google Vision, it is important to determine the associated expenses. However, regardless of the cost, it is crucial to ensure that your tests run seamlessly without any additional requests. Therefore, it is recommended to mock (simulate) the Google Vision API for testing purposes.

Remember, mocking is simulating the original behaviour, that’s it.

Mocking libraries

There are several mocking libraries available for PHP, including PHPUnit’s built-in mocking framework. However, in my opinion, Mockery provides a better mocking experience compared to PHPUnit. This is because Mockery has a more intuitive and natural language for setting up mock objects, which includes a set of matchers similar to Hamcrest. While PHPUnit’s mocking framework is also robust and effective, Mockery can be a more user-friendly and enjoyable option.

In this post, I’ll be using Mockery instead of PHPUnit, so go ahead and install mockery in your application:

composer require mockery/mockery

If you are using Laravel, it’s worth noting that Mockery is already included with the framework by default, so there is no need to install it.

CurrencyConversion

Back to the earlier post on CurrencyConversion, I tested the formatNumber method using a dummy object. This was because formatNumber does not call the API, as its sole functionality is to format numbers. However, how can we test the convert method, which does make an API call?

It’s important to note that testing the CurrencyConversion::convert in isolation would not be meaningful. Instead, we should test the usage of the CurrencyConversion::convert and how it is being utilized in the system.

Occasionally, it can be beneficial to the API call in isolation, for example verifying that the code accurately sends requests to the API and formats/returns the responses correctly.

Let’s take an example scenario where the CurrencyConversion is used in the OrderDecorator class to show the total order amount in the user’s local currency:

namespace App;

use App\Models\Order;

final class OrderDecorator
{
    public function __construct(private Order $order, private CurrencyConverstion $currencyConversion)
    {
    }

    public function amountWithUserCurrency(): string
    {
        return $this->currencyConversion->convert(
            from: $this->order->currency->code,
            to: $this->order->user->currency->code,
            amount: $this->order->amount,
        ) . $this->order->user->currency->code;
    }
}

When the user changes the currency, the order amount should be converted to the selected currency using the CurrencyConversion. For instance, if the user initially placed an order with a total of 50 EUR, but later decides to change the currency to DKK, the OrderDecorator should display the total amount in DKK, which in this case would be 375.50 DKK.

If you are a Laravel developer, you may have an Order model that utilizes a decorator, as shown below:

namespace App\Models;

use App\CurrencyConverstion;
use App\OrderDecorator;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function decorate(CurrencyConverstion $converstion)
    {
        return new OrderDecorator($this, $converstion);
    }
}

A note about the Service Container

To display the order’s amount in the user’s local currency, you might be implementing something similar to the following:

use App\Models\Order;
use App\CurrencyConversion;

$order = Order::find(178);

echo $order->decorate(new CurrencyConversion(new GuzzleHttp\Client));

One thing to note here is that each time we call the decorate method, we are required to pass in the CurrencyConversion. However, since the CurrencyConversion class relies on the GuzzleHttp\Client, we are also required to pass that in as well. This can make our code more difficult to maintain over time.

The better approach is to always fetch objects from the Service Container and avoid using the new keyword wherever possible. This ensures that all dependencies are managed by the container, making it easier to test your application.

In Laravel, we can change the decorate method in the Order class to use the app helper as follows:

public function decorate()
{
    return new OrderDecorator($this, app(CurrencyConverstion::class));
}

The ‘app’ helper function resolves a service (class) from the service container, and it also automatically resolves all dependencies used in the constructor of the ‘CurrencyConversion’ class.

Even if you are not using Laravel, it is highly recommended to use a service container in your application and avoid using the new keyword as much as possible. This is because relying on the new keyword can make your code harder to test and maintain over time.

Even though it is generally recommended to avoid using the new keyword and instead rely on the service container to manage your dependencies, in some cases it may be necessary to use the new keyword. In this case, I needed to pass an Order model which comes from the database - which as you guessed it cannot be resolved from the container -.

One potential alternative way to passing the Order object as a constructor parameter would be to remove it from the constructor and instead use a method like setOrder() to inject it later. However, for now, I’ll stick with using the constructor to create the OrderDecorator object with the necessary dependencies.

Laravel Service Container.

Mocking CurrencyConversion::convert

It’s time to test the CurrencyConversion functionality without making actual API calls:

use App\CurrencyConverstion;
use App\Models\Order;
use App\Models\User;
use Mockery;
use Mockery\MockInterface;

/** @test */
public function it_should_mock_currency_conversion_convert_method(): void
{
    $user = User::factory()->create();

    $order = Order::factory()->create([
        'user_id' => $user->id,
        'amount' => 198928,
        'currency' => 'EUR',
    ]);

    $mock = Mockery::mock(CurrencyConverstion::class, function(MockInterface $mock) {
        $mock->shouldReceive('convert')->andReturn(19198);
    });

    $this->instance(CurrencyConverstion::class, $mock);

    $this->assertEquals(
        expected: 19198, 
        actual: $order->decorate()->userCurrency()
    );
}

Let’ demystify the code:

To begin with, I’m creating new User and Order models in the database using what’s called Factories in Laravel. If you’re not familiar with Laravel, don’t worry, what’s happening here is that I’m creating new entries in the database, that’s it.

Moving on to the interesting part, I’m creating a new mocked object using the Mockery::mock method.

This method takes the name of the class that I want to mock as its first argument. The second argument is a closure that allows me to access the mocked object and manipulate it as needed.

Then, I use the shouldReceive method to configure the behavior of the mocked object.

The shouldReceive method can accept a string or an array of method names that we expect to be called during testing. Here, I’m instructing Mockery that whenever the convert method is called, it should simply return a predefined value of 19198. That’s it.

You can have more control, such as telling Mockery that the convert method will only be called once:

$mock->shouldReceive('convert')
    ->once() // convert should only be called once.
    ->andReturn(19198);

There are a variety of helpful methods that can be used to configure the behavior of mocked objects:

  • andReturn($value): instructs the mocked method to return the specified value when called.
  • andThrow($exception): instructs the mocked method to throw the specified exception when called.
  • with($arg1, $arg2, ...): specifies the arguments that the mocked method should expect when called.
  • once(), twice(), times($count) - specifies how many times the mocked method should be called.
  • allowing($method): configures the mocked object to allow calls to the specified method.
  • shouldNotReceive($method): configures the mocked object to not expect calls to the specified method.

These are just a few of the many methods provided by Mockery. For more information, I would recommend consulting the official Mockery documentation, which can be found here.

In the end, we’ll inject the newly mocked object into the service container, and this is where we can reap the benefits of using the service container that I mentioned earlier. In Laravel, you can achieve this by calling the $this->instance(…) method.

If you are a Laravel developer, it’s worth noting that Laravel comes with a built-in Http faker that can effectively mock http requests. As a result, using Mockery as we did might not be necessary.

That was all about Mcoking in PHP, see you in next post.