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 SubscribeLet's take a quick look back at GithubService
to see exactly what it's doing. First, the constructor requires an HttpClientInterface
object that we use to call GitHub. In return, we get back a ResponseInterface
that has an array of issue's for the dino-park
repository. Next we call the toArray()
method on the response, and iterate over each issue to see if the title contains the $dinosaurName
, so we can get its status label.
... lines 1 - 8 | |
class GithubService | |
{ | |
... lines 11 - 14 | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
... lines 17 - 18 | |
$response = $this->httpClient->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
... lines 23 - 28 | |
foreach ($response->toArray() as $issue) { | |
... lines 30 - 32 | |
} | |
... lines 34 - 35 | |
} | |
... lines 37 - 55 | |
} |
To get our tests to pass, we need to teach our fake httpClient
that when we call the request()
method, it should give back a ResponseInterface
object containing data that we control. So... let's do that.
Right after $mockHttpClient
, say $mockResponse = $this->createMock()
using ResponseInterface::class
for the class name. Below on $mockHttpClient
, call, ->method('request')
which willReturn($mockResponse)
. This tells our mock client that hey, anytime we call the request()
method on our mock, you need to return this $mockResponse
.
... lines 1 - 9 | |
use Symfony\Contracts\HttpClient\ResponseInterface; | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
... line 19 | |
$mockHttpClient = $this->createMock(HttpClientInterface::class); | |
$mockResponse = $this->createMock(ResponseInterface::class); | |
$mockHttpClient | |
->method('request') | |
->willReturn($mockResponse) | |
; | |
... lines 27 - 30 | |
} | |
... lines 32 - 44 | |
} |
We could run our tests now, but they would fail. We taught our mock client what it should return when we call the request()
method. But, now we need to teach our $mockResponse
what it needs to do when we call the toArray()
method. So right above, lets teach the $mockResponse
that when we call, method('toArray')
and it willReturn()
an array of issues. Because that's what GitHub returns when we call the API.
... lines 1 - 9 | |
use Symfony\Contracts\HttpClient\ResponseInterface; | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
... line 19 | |
$mockHttpClient = $this->createMock(HttpClientInterface::class); | |
$mockResponse = $this->createMock(ResponseInterface::class); | |
$mockResponse | |
->method('toArray') | |
->willReturn([]) | |
; | |
$mockHttpClient | |
->method('request') | |
->willReturn($mockResponse) | |
; | |
... lines 32 - 35 | |
} | |
... lines 37 - 49 | |
} |
For each issue, GitHub gives us the issue's "title", and among other things, an array of "labels". So let's mimic GitHub and make this array include one issue that has 'title' => 'Daisy'
.
And, for the test, we'll pretend she sprained her ankle so add a labels
key set to an array, that includes 'name' => 'Status: Sick'
.
Let's also create a healthy dino so we can assert that our parsing checks that correctly too. Copy this issue and paste it below. Change Daisy
to Maverick
and set his label to Status: Healthy
.
... lines 1 - 9 | |
use Symfony\Contracts\HttpClient\ResponseInterface; | |
class GithubServiceTest extends TestCase | |
{ | |
... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
... line 19 | |
$mockHttpClient = $this->createMock(HttpClientInterface::class); | |
$mockResponse = $this->createMock(ResponseInterface::class); | |
$mockResponse | |
->method('toArray') | |
->willReturn([ | |
[ | |
'title' => 'Daisy', | |
'labels' => [['name' => 'Status: Sick']], | |
], | |
[ | |
'title' => 'Maverick', | |
'labels' => [['name' => 'Status: Healthy']], | |
], | |
]) | |
; | |
$mockHttpClient | |
->method('request') | |
->willReturn($mockResponse) | |
; | |
... lines 41 - 44 | |
} | |
... lines 46 - 58 | |
} |
Perfect! Our assertions are already expecting Daisy
to be sick and Maverick
to be healthy. So, if our tests pass, it means that all of our label-parsing logic is correct.
Fingers crossed, let's try it:
./vendor/bin/phpunit
And... Awesome! They are passing! And the best part about it, we're no longer calling GitHub's API when we run our tests! Imagine the panic we would cause if we had to lock down the park because our tests failed due to the api being offline... or just someone changing the labels up on GitHub, Ya... I don't want that headache either...
Remember when we were talking about the different names for mocks? Welp, both mockResponse
and mockHttpClient
are now officially called stubs... That's a fancy way of saying fake objects where we optionally take control of the values it returns. That's exactly what we are doing with the willReturn()
method. Again, the terminology isn't too important, but there you go. These are stubs. And yes, every time I teach this, I need to look up these terms to remember exactly what they mean.
Up next, we're going to turn our stubs into full-blown mock objects by also testing the data passed into the mock.
Hey @Felipe-L
In that case, you'd have to make a real API call to GitHub. I don't see a value in doing a health check of the API on your tests because that's outside of your control, it's better to have a retry mechanism or the ability to tolerate failures (disable the feature on the frontend)
Cheers!
Hi @MolloKhan, thanks for your reply.
I got what you said and agree that's not on our control to test an API call.
Lets say my application depends on this feature to work properly. Would be a good idea to mark the test as skipped if it's not possible to return the content from the API?
Then have another mechanism to let us know that the API is unavailable?
thanks
I think we're talking about two things.
1) Testing a real API response to be aware of changes. In this case, your test will need to make a real request to the API, that's sometimes fine, but keep in mind that those tests are slow, so you want to have just a handful of them
2) API health status. You can add a test where you mock out the response, and it will always return an error so you can test how your application behaves in that circumstance. For example, you could show a warning message on the frontend
Also, as I said earlier you can add a retry mechanism. This can be easily integrated with Symfony Messenger
Cheers!
What's up with all the !!!!!!?
use.....!!!!!!!!!!!!!!!!!!! And, for the test, we'll pretend she sprained her ankle so add a labels key set to an array, that includes 'name' => 'Status: Sick'
!!!!!!!!!! Zoom back to the Github Issues to "circling" Maverick!!!!!!!!!!!!!!!!!!! Let's also cr...
Hey Tac!
Whoops, those are some dev comments we used when had been creating this tutorial. I fixed it in https://github.com/SymfonyCasts/testing/commit/2248a2b70b0fad08f2077271d6c3f4d7829d2473 , thank you for reporting it :)
Cheers!
There is another instance of this.
!!!!!!!!! This chapter is short enough, we could? run the tests !!!!!!!!!
around 1:22 min of the video
Ah! The script was fixed, but the subtitles need to be regenerated. We'll get on that! Thanks for the note :)
// 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
}
}
Hi,
great tutorial so far, quite enjoyable.
I've got a question related to testing response from real service.
In this case, we won't call github api because we don't control it and our test fails if it's unavailable, for example.
Mocking the Response will make sure tests are going to pass but the application won't have the correct status.
For example, the API is unavailable or changed its array structure, tests will pass but I'll never find out the application is broke (it won't be able to get health status from github).
My question is: is there a way to test the scenario?
thanks