Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Setup and Tearing It Down

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 continue refactoring our test. In the test method, we create a MockResponse, MockHttpClient, and instantiate GitHubService with a mock LoggerInterface. We're doing the same thing in this test above. Didn't Ryan say to DRY out our code in another tutorial? Fine... I suppose we'll listen to him.

Start by adding three private properties to our class: a LoggerInterface $mockLogger, followed by MockHttpClient $mockHttpClient and finally MockResponse $mockresponse:

... lines 1 - 13
class GithubServiceTest extends TestCase
{
private LoggerInterface $mockLogger;
private MockHttpClient $mockHttpClient;
private MockResponse $mockResponse;
... lines 19 - 93
}

At the bottom of the test, create a private function createGithubService() that requires array $responseData then returns GithubService. Inside, say $this->mockResponse = new MockResponse() that json_encode()'s the $responseData:

... lines 1 - 13
class GithubServiceTest extends TestCase
{
private LoggerInterface $mockLogger;
private MockHttpClient $mockHttpClient;
private MockResponse $mockResponse;
... lines 19 - 85
private function createGithubService(array $responseData): GithubService
{
$this->mockResponse = new MockResponse(json_encode($responseData));
... lines 89 - 92
}
}

Since we'll be creating the MockResponse after we instantiate the MockHttpClient, which you'll see in a second, we need to pass our response to the client without using the client's constructor. To do that, we can say $this->mockHttpClient->setResponseFactory($this->mockResponse). Finally return a new GithubService() with $this->mockHttpClient and $this->mockLogger.

... lines 1 - 13
class GithubServiceTest extends TestCase
{
private LoggerInterface $mockLogger;
private MockHttpClient $mockHttpClient;
private MockResponse $mockResponse;
... lines 19 - 85
private function createGithubService(array $responseData): GithubService
{
$this->mockResponse = new MockResponse(json_encode($responseData));
$this->mockHttpClient->setResponseFactory($this->mockResponse);
return new GithubService($this->mockHttpClient, $this->mockLogger);
}
}

We could use a constructor to instantiate our mocks and set them on those properties. But PHPUnit will only instantiate our test class once, no matter how many test methods it has. And we want to make sure we have fresh mock objects for each test run. How can we do that? At the top, add protected function setUp(). Inside, say $this->mockLogger = $this->createMock(LoggerInterface::class) then $this->mockHttpClient = new MockHttpClient().

... lines 1 - 13
class GithubServiceTest extends TestCase
{
private LoggerInterface $mockLogger;
private MockHttpClient $mockHttpClient;
private MockResponse $mockResponse;
protected function setUp(): void
{
$this->mockLogger = $this->createMock(LoggerInterface::class);
$this->mockHttpClient = new MockHttpClient();
}
... lines 25 - 88
private function createGithubService(array $responseData): GithubService
{
$this->mockResponse = new MockResponse(json_encode($responseData));
$this->mockHttpClient->setResponseFactory($this->mockResponse);
return new GithubService($this->mockHttpClient, $this->mockLogger);
}
}

Down in the test method, cut the response array, then say $service = $this->createGithubService() and paste the array.

... lines 1 - 13
class GithubServiceTest extends TestCase
{
private LoggerInterface $mockLogger;
private MockHttpClient $mockHttpClient;
private MockResponse $mockResponse;
protected function setUp(): void
{
$this->mockLogger = $this->createMock(LoggerInterface::class);
$this->mockHttpClient = new MockHttpClient();
}
... lines 25 - 73
public function testExceptionThrownWithUnknownLabel(): void
{
$service = $this->createGithubService([
[
'title' => 'Maverick',
'labels' => [['name' => 'Status: Drowsy']],
],
]);
... lines 82 - 85
$service->getHealthReport('Maverick');
}
private function createGithubService(array $responseData): GithubService
{
$this->mockResponse = new MockResponse(json_encode($responseData));
$this->mockHttpClient->setResponseFactory($this->mockResponse);
return new GithubService($this->mockHttpClient, $this->mockLogger);
}
}

Let's see how our tests are doing in the terminal...

./vendor/bin/phpunit

And... Ya! Everything is looking good!

The idea is pretty simple: if your test class has a method called setUp(), PHPUnit will call it before each test method, which gives us fresh mocks at the start of every test. Need to do something after each test? Same thing: create a method called tearDown(). This isn't as common... but you might do it if you want to clean up some filesystem changes that were made during the test. In our case, there's no need.

In addition to setUp() and tearDown(), PHPUnit also has a few other methods, like setUpBeforeClass() and tearDownAfterClass(). These are called once per class, and we'll get more into those as they become relevant in future tutorials. And if you were wondering, these methods are called "Fixture Methods" because they help setup any "fixtures" to get your environment into a known state for your test.

Anyhow, let's get back to refactoring. For the first test in this class, cut out the response array, select all of this "dead code", add $service = $this->createGithubService() then paste in the array. We can remove the $service variable below:

... lines 1 - 11
class GithubServiceTest extends TestCase
{
... lines 14 - 26
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void
{
$service = $this->createGithubService([
[
'title' => 'Daisy',
'labels' => [['name' => 'Status: Sick']],
],
[
'title' => 'Maverick',
'labels' => [['name' => 'Status: Healthy']],
],
]);
... lines 39 - 43
}
... lines 45 - 81
}

But now we need to figure out how to keep these expectations that we were using on the old $mockHttpClient. Being able to test that we only call GitHub once with the GET HTTP Method and that we're using the right URL, is pretty valuable.

Fortunately, those mock classes have special code just for this. Below, assertSame() that 1 is identical to $this->mockHttpClient->getRequestCount() then assertSame() that GET is identical to $this->mockResponse->getRequestMethod(). Finally, copy and paste the URL into assertSame() and call getRequestUrl() on mockResponse. Remove the old $mockHttpClient... and the use statements that we're no longer using up top.

... lines 1 - 11
class GithubServiceTest extends TestCase
{
... lines 14 - 26
public function testGetHealthReportReturnsCorrectHealthStatusForDino(HealthStatus $expectedStatus, string $dinoName): void
{
$service = $this->createGithubService([
[
'title' => 'Daisy',
'labels' => [['name' => 'Status: Sick']],
],
[
'title' => 'Maverick',
'labels' => [['name' => 'Status: Healthy']],
],
]);
self::assertSame($expectedStatus, $service->getHealthReport($dinoName));
self::assertSame(1, $this->mockHttpClient->getRequestsCount());
self::assertSame('GET', $this->mockResponse->getRequestMethod());
self::assertSame('https://api.github.com/repos/SymfonyCasts/dino-park/issues', $this->mockResponse->getRequestUrl());
}
... lines 45 - 81
}

Alrighty, time to check the fences...

./vendor/bin/phpunit

And... Wow! Everything is still green!

Welp, there you have it... We've helped Bob improve Dinotopia by adding a few small features to the app. But more importantly, we're able to test that those features are working as we intended. Is there more work to be done? Absolutely! We're going to take our app to the next level by adding a persistence layer to store dinos in the database and learn how to write tests for that too. These tests, where you use real database connections or make real API calls, instead of mocking, are sometimes called integration tests. That's the topic of the next tutorial in this series.

I hope you enjoyed your time here at the park - and thanks for keeping your arms and legs inside the vehicle at all times. If you have any questions, suggestions, or want to ride with Big Eaty in the Jeep - just leave us a comment. Alright, see you in the next episode!

Leave a comment!

0
Login or Register to join the conversation
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
    }
}