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!

4
Login or Register to join the conversation
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted 21 days 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
    }
}