If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHaving the ability to mock objects in tests is super awesome, and kind of weird and complex all at the same time. If we have simple objects, like Dinosaur
, we should avoid the extra lines of code and just instantiate a real Dinosaur
for the test. After all, it's pretty easy to control the behavior of Dinosaur
just by calling its real methods. No need for the mock weirdness.
But, for more complex objects, like HttpClient
, using the real client... can be a headache. The general rule of thumb is to use mocks for complex objects like, services... but for simple objects, like models or entities, just use the real thing.
Looking back at Symfony's HTTP Client, we were able to mock both the client and the response to control its behavior. But, because needing to do this sort of thing is so common, Symfony's HTTP Client comes with some special classes that can do the mocking for us. It comes with two real classes specifically made for testing: MockHttpClient
& MockResponse
. Using PHPUnit's mock system is fine, but these exist to make our life even easier.
Check it out: instead of creating a mock for $mockResponse
, instantiate a MockResponse()
passing in json_encode()
with an array to mimic GitHub's API response. Grab Maverick's issue below and paste that into the array. Since MockResponse
is already configured, remove the $mockResponse
bits below.
... lines 1 - 8 | |
use Symfony\Component\HttpClient\MockHttpClient; | |
use Symfony\Component\HttpClient\Response\MockResponse; | |
... lines 11 - 13 | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 16 - 63 | |
public function testExceptionThrownWithUnknownLabel(): void | |
{ | |
$mockResponse = new MockResponse(json_encode([ | |
[ | |
'title' => 'Maverick', | |
'labels' => [['name' => 'Status: Drowsy']], | |
], | |
])); | |
... lines 72 - 79 | |
} | |
} |
For the client, remove $mockHttpClient
and below, instantiate a new MockHttpClient()
passing in $mockResponse
instead. Since we're not doing anything with $mockLogger
, cut createMock()
, remove the variable, and paste that as an argument to GithubService()
.
... lines 1 - 8 | |
use Symfony\Component\HttpClient\MockHttpClient; | |
use Symfony\Component\HttpClient\Response\MockResponse; | |
... lines 11 - 13 | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 16 - 63 | |
public function testExceptionThrownWithUnknownLabel(): void | |
{ | |
$mockResponse = new MockResponse(json_encode([ | |
[ | |
'title' => 'Maverick', | |
'labels' => [['name' => 'Status: Drowsy']], | |
], | |
])); | |
$mockHttpClient = new MockHttpClient($mockResponse); | |
$service = new GithubService($mockHttpClient, $this->createMock(LoggerInterface::class)); | |
... lines 75 - 79 | |
} | |
} |
Wow, this is looking better already! But, let's see what happens when we run the tests:
./vendor/bin/phpunit
And... Woah! All of the tests are passing!
But, the assertion count did go down to "16" because MockHttpClient
and MockResponse
do not actually perform any assertions, like how many times a method is called.
So... what's the actual benefit to using these mock classes? Why not just create our own via PHPUnit? Ha... Check out this diff of GithubService
. We removed 11 lines of code by using the "built-in" mocks in just one test. Imagine how many lines of code we could remove by using them in all of our tests. Hm... I think that's exactly what we'll do next.
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": ">=8.1.0",
"ext-ctype": "*",
"ext-iconv": "*",
"symfony/asset": "6.1.*", // v6.1.0
"symfony/console": "6.1.*", // v6.1.4
"symfony/dotenv": "6.1.*", // v6.1.0
"symfony/flex": "^2", // v2.2.3
"symfony/framework-bundle": "6.1.*", // v6.1.4
"symfony/http-client": "6.1.*", // v6.1.4
"symfony/runtime": "6.1.*", // v6.1.3
"symfony/twig-bundle": "6.1.*", // v6.1.1
"symfony/yaml": "6.1.*" // v6.1.4
},
"require-dev": {
"phpunit/phpunit": "^9.5", // 9.5.23
"symfony/browser-kit": "6.1.*", // v6.1.3
"symfony/css-selector": "6.1.*", // v6.1.3
"symfony/phpunit-bridge": "^6.1" // v6.1.3
}
}