Mocking: Mock Objects
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur tests are passing, the dino's are wandering, and life is great! But... let's think about this for a second. In GithubService
, when we test getHealthReport()
, we're able to control the $response
that we get back from request()
by using a stub. That's great, but it might also be nice to ensure that the service is only calling GitHub one time and that it's using the right HTTP method with the correct URL. Could we do that? Absolutely!
Expect a Method to Be Called
In GithubServiceTest
where we configure the $mockHttpClient
, add ->expects()
, and pass self::once()
.
// ... lines 1 - 11 | |
class GithubServiceTest extends TestCase | |
{ | |
// ... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
// ... lines 19 - 36 | |
$mockHttpClient | |
->expects(self::once()) | |
// ... lines 39 - 40 | |
; | |
// ... lines 42 - 45 | |
} | |
// ... lines 47 - 59 | |
} |
Over in the terminal, run our tests...
./vendor/bin/phpunit
Expecting Specific Arguments
And... Awesome! We've just added an assertion to our mock client that requires the request
method be called exactly once. Let's take it a step further and add ->with()
passing GET
... and then I'll paste the URL to the GitHub API.
// ... lines 1 - 11 | |
class GithubServiceTest extends TestCase | |
{ | |
// ... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
// ... lines 19 - 36 | |
$mockHttpClient | |
->expects(self::once()) | |
->method('request') | |
->with('GET', 'https://api.github.com/repos/SymfonyCasts/dino-park') | |
->willReturn($mockResponse) | |
; | |
// ... lines 43 - 46 | |
} | |
// ... lines 48 - 60 | |
} |
Try the tests again...
./vendor/bin/phpunit
And... Huh! We have 2 failures:
Failed asserting that two strings are equal
Hmm... Ah Ha! My copy and paste skills are a bit weak. I missed /issue
at the end of the URL. Add that.
// ... lines 1 - 11 | |
class GithubServiceTest extends TestCase | |
{ | |
// ... lines 14 - 16 | |
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void | |
{ | |
// ... lines 19 - 36 | |
$mockHttpClient | |
// ... lines 38 - 39 | |
->with('GET', 'https://api.github.com/repos/SymfonyCasts/dino-park/issues') | |
// ... line 41 | |
; | |
// ... lines 43 - 46 | |
} | |
// ... lines 48 - 60 | |
} |
Let's see if that was the trick:
./vendor/bin/phpunit
Umm... Yes! We're green all day. But best of all, the tests confirm we're using the correct URL and HTTP method when we call GitHub.
But... What if we actually wanted to call GitHub more than just once? Or... we wanted to assert that it was not called at all? PHPUnit has us covered. There are a handful of other methods we can call. For example, change once()
to never()
.
And watch what happens now:
./vendor/bin/phpunit
Hmm... Yup, we have two failures because:
request() was not expected to be called.
That's really nifty! Change the expects()
back to once()
and just to be sure we didn't break anything - run the tests again.
./vendor/bin/phpunit
And... Awesome!
Carefully Applying Assertions
We could call expects()
on our $mockResponse
to make sure that toArray()
is being called exactly once in our service. But, do we really care? If it's not being called at all, our test would certainly fail. And if it's being called twice, no big deal! Using ->expects()
and ->with()
are great ways to add extra assertions... when you need them. But no need to micromanage how many times something is called or its arguments if that is not so important.
Using GitHubService in our App
Now that GithubService
is fully tested, we can celebrate by using it to drive our dashboard! On MainController::index()
, add an argument: GithubService $github
to autowire the new service.
// ... lines 1 - 5 | |
use App\Service\GithubService; | |
// ... lines 7 - 10 | |
class MainController extends AbstractController | |
{ | |
path: '/', name: 'main_controller', methods: ['GET']) | (|
public function index(GithubService $github): Response | |
{ | |
// ... lines 16 - 30 | |
} | |
} |
Next, right below the $dinos
array, foreach()
over $dinos as $dino
and, inside say $dino->setHealth()
passing $github->getHealthReport($dino->getName())
.
// ... lines 1 - 5 | |
use App\Service\GithubService; | |
// ... lines 7 - 10 | |
class MainController extends AbstractController | |
{ | |
path: '/', name: 'main_controller', methods: ['GET']) | (|
public function index(GithubService $github): Response | |
{ | |
// ... lines 16 - 23 | |
foreach ($dinos as $dino) { | |
$dino->setHealth($github->getHealthReport($dino->getName())); | |
} | |
// ... lines 27 - 30 | |
} | |
} |
To the browser and refresh...
And... What!
getDinoStatusFromLabels()
must beHealthStatus
,null
returned
What's going on here? By the way, the fact that our unit test passes but our page fails can sometimes happen and in a future tutorial, we'll write a functional test to make sure this page actually loads.
The error isn't very obvious, but I think one of our dino's has a status label that we don't know about. Let's peek back at the issues on GitHub and... HA! "Dennis" is causing problems yet again. Apparently he's a bit hungry...
In our HealthStatus
enum, we don't have a case for Hungry
status labels. Go figure. Is a hungry dinosaur accepting visitors? I don't know - I guess it depends on if you ask the visitor or the dino. Anyways, Hungry
is not a status we expected. So next, let's throw a clear exception if we run into an unknown status and test for that exception.
Is there a relation between ->with() and ->willReturn() ?
Reading:
...->with('GET', 'https://api.github.com/repos/SymfonyCasts/dino-park/issues')
->willReturn($mockResponse);
The response will not actually return the $mockResponse, are those two calls in the chain independent? Reading it seems confusing. I guess willReturn() is more of a statement than an expectation, right?