A dummy is an object that replaces all the original methods with a dummy implementation that returns null (without calling the original method).
You can create a dummy object using the createMock method in PHPUnit:
use GuzzleHttp\Client;
$client = $this->createMock(Client::class);
Inspecting the dummy object using dd($client);:
Mock_Client_8cd31d5b {#129
-config: null
-__phpunit_originalObject: null
-__phpunit_returnValueGeneration: true
-__phpunit_invocationMocker: null
}
The createMock method creates a random class -- in this case it was named Mock_Client_8cd31d5b. This class extends GuzzleHttp\Client:
dd(
class_parents($this->createMock(Client::class)),
);
Output:
array:1 [
"GuzzleHttp\Client" => "GuzzleHttp\Client"
]
It also overrides all the original methods so they return null:
$client = $this->createMock(Client::class);
// This line returns null
dd(
$client->get('https://example.com')->getBody()
);
Note that createMock replaces all objects recursively. The get method will return a new dummy implementation of Psr\Http\Message\ResponseInterface.
Now that you know what dummies are, here is how to use them.
Make the following changes to the CurrencyConvertor class:
<?php
namespace App;
use Exception;
use GuzzleHttp\Client;
class CurrencyConversion
{
public function __construct(private Client $client)
{
}
public function convert(string $from, string $to, int|float $amount): float
{
$query = http_build_query([
'from' => $from,
'to' => $to,
'amount' => $amount,
]);
$req = $this
->client
->request('GET', 'https://api.exchangerate.host/convert?'.$query);
if ($req->getStatusCode() !== 200) {
throw new Exception('Could not convert!');
}
$res = json_decode($req->getBody()->getContents());
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid response!');
}
return $this->formatNumber($res->result);
}
public function formatNumber(float $amount, int $decimals = 2): float
{
return (float) number_format($amount, $decimals);
}
}
The number_format logic is now in its own method.
Here is how to test the formatNumber method:
public function test_format_number()
{
$client = $this->createMock(Client::class);
$currencyConvertor = new CurrencyConversion($client);
$this->assertEqualsWithDelta(
10.99,
$currencyConvertor->formatNumber(10.999)
, 0.1);
}
CurrencyConvertor::__construct requires an instance of GuzzleHttp\Client but does not use it unless you call the convert method.
Stubs
The practice of replacing an object with a test double that (optionally) returns configured return values is referred to as stubbing. PHPUnit.
In the previous post, we mocked different objects, for example the response object:
$response = $this->createMock(ResponseInterface::class);
$response
->method('getStatusCode')
->willReturn(200);
This is stubbing because you are configuring an object that returns a predefined value when called.
Conclusion
The difference between Dummies and Stubs lies in the return value. A Dummy always returns null, whereas a Stub returns a predefined value.
PHPUnit supports a number of stubbing methods such as willReturnArgument, willReturnSelf, willThrowException. Consult the documentation to learn more about them.
Summary
- Dummy -- a test double that replaces all original methods with implementations returning
null, created viacreateMock(). - Stub -- a test double configured to return specific predefined values when its methods are called.
- Key difference -- Dummies return
nullby default; Stubs return values you explicitly configure. - createMock -- recursively replaces all objects, so chained method calls on a dummy also return dummy implementations.