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.
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.