Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Mocking: Stubs

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

Training the Mock on what to Return

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

Stubs? Mocks?

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.

Leave a comment!

8
Login or Register to join the conversation
Felipe-L Avatar
Felipe-L Avatar Felipe-L | posted 6 days ago

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

Reply

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!

Reply
Felipe-L Avatar

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

Reply

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!

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted 9 months ago

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

Reply

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!

Reply

There is another instance of this.
!!!!!!!!! This chapter is short enough, we could? run the tests !!!!!!!!!
around 1:22 min of the video

Reply

Ah! The script was fixed, but the subtitles need to be regenerated. We'll get on that! Thanks for the note :)

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice