Mocking is used to test code that depends on external services that are unreliable or difficult to test directly, such as third-party APIs.
By simulating the behavior of these services using mock objects, you ensure that the code works as expected without calling the real API.
Consider using Google Vision in your project. If you want to test the code that depends on Google Vision, you need to think about cost and reliability. Your tests should run without making additional requests. The right approach is to mock (simulate) the Google Vision API for testing purposes.
Mocking is simulating the original behaviour. Nothing more.
Mocking libraries
There are several mocking libraries available for PHP, including PHPUnit's built-in mocking framework. Mockery provides a better mocking experience compared to PHPUnit. Mockery has a more intuitive and natural language for setting up mock objects, which includes a set of matchers similar to Hamcrest. PHPUnit's mocking framework is robust and effective, but Mockery is more user-friendly.
In this post, I will be using Mockery instead of PHPUnit. Install Mockery in your application:
composer require mockery/mockery
If you are using Laravel, Mockery is already included with the framework by default. No need to install it.
CurrencyConversion
Back to the earlier post on CurrencyConversion, I tested the formatNumber method using a dummy object. That worked because formatNumber does not call the API -- its sole functionality is to format numbers. But how do you test the convert method, which does make an API call?
Testing CurrencyConversion::convert in isolation is not meaningful. You should test how CurrencyConversion::convert is being utilized in the system.
Occasionally, testing the API call in isolation is beneficial -- for example, verifying that the code accurately sends requests to the API and formats/returns the responses correctly.
Here is an example scenario where 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 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 -- in this case, 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 would implement something like this:
use App\Models\Order;
use App\CurrencyConversion;
$order = Order::find(178);
echo $order->decorate(new CurrencyConversion(new GuzzleHttp\Client));
One problem here: each time you call the decorate method, you are required to pass in the CurrencyConversion. Since CurrencyConversion relies on GuzzleHttp\Client, you need to pass that in as well. This makes the code harder 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, change the decorate method in the Order class to use the app helper:
public function decorate()
{
return new OrderDecorator($this, app(CurrencyConverstion::class));
}
The app helper function resolves a service (class) from the service container and automatically resolves all dependencies used in the constructor of the CurrencyConversion class.
Even if you are not using Laravel, use a service container in your application and avoid the new keyword as much as possible. Relying on the new keyword makes your code harder to test and maintain over time.
Even though you should avoid the new keyword and rely on the service container, in some cases it is necessary. Here, I needed to pass an Order model which comes from the database -- it cannot be resolved from the container.
One alternative to passing the Order object as a constructor parameter is to remove it from the constructor and use a method like setOrder() to inject it later. For now, I will stick with using the constructor to create the OrderDecorator object with the necessary dependencies.
Mocking CurrencyConversion::convert
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()
);
}
Here is what the code does:
First, I am creating new User and Order models in the database using Factories in Laravel. If you are not familiar with Laravel, all that is happening here is creating new entries in the database.
The interesting part: I am creating a new mocked object using the Mockery::mock method.
This method takes the name of the class to mock as its first argument. The second argument is a closure that gives you access to the mocked object so you can manipulate it as needed.
Then, the shouldReceive method configures the behavior of the mocked object.
shouldReceive accepts a string or an array of method names that you expect to be called during testing. Here, I am instructing Mockery that whenever the convert method is called, it should return a predefined value of 19198.
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 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 a few of the many methods provided by Mockery. For more information, consult the official Mockery documentation here.
Finally, inject the newly mocked object into the service container -- this is where you reap the benefits of using the service container mentioned earlier. In Laravel, you achieve this by calling the $this->instance(…) method.
If you are a Laravel developer, Laravel comes with a built-in Http faker that can effectively mock http requests. Using Mockery as shown above is not always necessary.
Summary
- Mocking -- simulating external service behavior so your tests run without making real API calls.
- Mockery -- a PHP mocking library with intuitive, natural-language syntax for setting up mock objects, preferred over PHPUnit's built-in mocking framework.
- Service Container -- fetch objects from the container instead of using the
newkeyword directly, which makes your code easier to test and maintain. - shouldReceive -- the core Mockery method for configuring what a mocked method should return, throw, or how many times it should be called.
- Injecting mocks -- replace real dependencies with mocked ones in the service container using
$this->instance()in Laravel.