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 SubscribeBob just told us he needs to display which dinos are accepting lunch in our app... I mean accepting visitors. GenLab has strict protocols in place: park guests can visit with healthy dinos... but if they're sick, no visitors allowed. To help display this, we need to store the health status of each dino and have an easy way to figure out whether or not this means they're accepting visitors...
Let's start by adding a method - isAcceptingVisitors()
to Dinosaur
. But, we'll do this the TDD way by writing the test first. In DinosaurTest
add public function testIsAcceptingVisitorsByDefault()
. Inside, $dino = new Dinosaur()
and let's call him Dennis
:
... lines 1 - 7 | |
class DinosaurTest extends TestCase | |
{ | |
... lines 10 - 41 | |
public function testIsAcceptingVisitorsByDefault(): void | |
{ | |
$dino = new Dinosaur('Dennis'); | |
... lines 45 - 46 | |
} | |
} |
If we simply instantiate a Dinosaur
and do nothing else, GenLab policy states that it is ok to visit that Dinosaur. So assertTrue()
that Dennis isAcceptingVisitors()
:
... lines 1 - 7 | |
class DinosaurTest extends TestCase | |
{ | |
... lines 10 - 41 | |
public function testIsAcceptingVisitorsByDefault(): void | |
{ | |
$dino = new Dinosaur('Dennis'); | |
self::assertTrue($dino->isAcceptingVisitors()); | |
} | |
} |
Below this test, add another called testIsNotAcceptingVisitorsIfSick()
. And for now, let's be lazy and just say $this->markTestIncomplete()
:
... lines 1 - 7 | |
class DinosaurTest extends TestCase | |
{ | |
... lines 10 - 48 | |
public function testIsNotAcceptingVisitorsIfSick(): void | |
{ | |
... lines 51 - 55 | |
} | |
} |
Ok, let's try the tests:
./vendor/bin/phpunit --testdox
And... no surprise! Our first new test is failing:
Call to an undefined method.
But, our next test has this weird circle ∅
because we marked the test as incomplete. I use this sometimes when I know I need to write a test... I'm just not ready to do it quite yet. PHPUnit also has a markSkipped()
method that can be used to skip tests under certain conditions, like if a test should run on PHP 8.1.
Anywho, let's get back to coding, shall we... In our Dinosaur
class, add a isAcceptingVisitors()
method that returns a bool
, and inside we'll return true
.
... lines 1 - 4 | |
class Dinosaur | |
{ | |
... lines 7 - 52 | |
public function isAcceptingVisitors(): bool | |
{ | |
return true; | |
} | |
} |
Let's see what happens when we run our tests now...
./vendor/bin/phpunit --testdox
And... Yes! Is accepting visitors by default
... is now passing! We still have one incomplete test as a reminder, but it's not causing our whole test suite to fail.
Let's finish that now. If we peek at the issues on GitHub - GenLab is using labels to identify the "health" of each dino: "Sick" versus "Healthy". Pretty soon, we're going to read these labels and use them in our app. To prep for that, we need a way to store the current health on each Dinosaur
.
Inside the test, remove markAsIncomplete()
and create a $dino
named Bumpy
... he's a triceratops. Now call $dino->setHealth('Sick')
and then assertFalse()
that Bumpy isAcceptingVisitors()
. He's cranky when he's sick.
... lines 1 - 7 | |
class DinosaurTest extends TestCase | |
{ | |
... lines 10 - 48 | |
public function testIsNotAcceptingVisitorsIfSick(): void | |
{ | |
$dino = new Dinosaur('Bumpy'); | |
$dino->setHealth('Sick'); | |
self::assertFalse($dino->isAcceptingVisitors()); | |
} | |
} |
But, no surprise, PHPStorm is telling us:
Method setHealth() not found inside Dinosaur
So... let's skip running the test and head straight to Dinosaur
to add a setHealth()
method that accepts a string $health
argument... and returns void
. Inside, say $this->health = $health
... then up top, add a private string $health
property that defaults to Healthy
:
... lines 1 - 4 | |
class Dinosaur | |
{ | |
... lines 7 - 10 | |
private string $health = 'Healthy'; | |
... lines 12 - 58 | |
public function setHealth(string $health): void | |
{ | |
$this->health = $health; | |
} | |
} |
Cool! Now we just need to update isAcceptingVisitors()
to return $this->health === $healthy
instead of true
:
... lines 1 - 4 | |
class Dinosaur | |
{ | |
... lines 7 - 53 | |
public function isAcceptingVisitors(): bool | |
{ | |
return $this->health === 'Healthy'; | |
} | |
... lines 58 - 62 | |
} |
Fingers crossed our tests are now passing...
./vendor/bin/phpunit --testdox
And... Mission Accomplished!
Now that the tests are passing, I'm thinking we should refactor the setHealth()
method to only allow Sick
or Healthy
... and not something like Dancing
... Inside src/
, create a new Enum/
directory then a new class: HealthStatus
. For the template, select Enum
and click OK
. We need HealthStatus
to be backed by a : string
...
... lines 1 - 2 | |
namespace App\Enum; | |
enum HealthStatus: string | |
{ | |
... lines 7 - 8 | |
} |
And our first case HEALTHY
will return Healthy
, then case SICK
will return Sick
.
... lines 1 - 2 | |
namespace App\Enum; | |
enum HealthStatus: string | |
{ | |
case HEALTHY = 'Healthy'; | |
case SICK = 'Sick'; | |
} |
On the Dinosaur::$health
property, default to HealthStatus::HEALTHY
. And change the property type to HealthStatus
. Down in isAcceptingVisitors()
, return true if $this->health === HealthStatus::HEALTHY
. Below in setHealth()
, change the argument type from string
to HealthStatus
.
... lines 1 - 4 | |
use App\Enum\HealthStatus; | |
class Dinosaur | |
{ | |
... lines 9 - 12 | |
private HealthStatus $health = HealthStatus::HEALTHY; | |
... lines 14 - 55 | |
public function isAcceptingVisitors(): bool | |
{ | |
return $this->health === HealthStatus::HEALTHY; | |
} | |
public function setHealth(HealthStatus $health): void | |
{ | |
$this->health = $health; | |
} | |
} |
The last thing to do is use HealthStatus::SICK
in our test.
... lines 1 - 5 | |
use App\Enum\HealthStatus; | |
... lines 7 - 8 | |
class DinosaurTest extends TestCase | |
{ | |
... lines 11 - 49 | |
public function testIsNotAcceptingVisitorsIfSick(): void | |
{ | |
... lines 52 - 53 | |
$dino->setHealth(HealthStatus::SICK); | |
... lines 55 - 56 | |
} | |
} |
Let's see if we broke anything!
./vendor/bin/phpunit --testdox
And... Ya! We didn't break anything... I'm only a little surprised.
To fulfill Bob's wishes, open the main/index.html.twig
template and add an Accepting Visitors
heading to the table. In the dino loop, create a new <td>
and call dino.acceptingVisitors
. We'll show Yes
if this is true or No
if we get false.
... lines 1 - 3 | |
<div class="container volcano mt-4" style="flex-grow: 1;"> | |
... line 5 | |
<div class="dino-stats-container mt-2 p-3"> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
... lines 10 - 13 | |
<th>Accepting Visitors</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for dino in dinos %} | |
<tr> | |
... lines 20 - 23 | |
<td>{{ dino.acceptingVisitors ? 'Yes' : 'No' }}</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
</div> | |
... lines 31 - 53 |
In the browser, refresh the status page... And... WooHoo! All of our dinos are accepting visitors... because we haven't set any as "sick" on our code!
But... We already know from looking at GitHub earlier, that some of our dinos are sick. Next: let's use GitHub's API to read the labels from our GitHub repository and set the real health on each Dinosaur
so that our dashboard will update in real-time.
"Houston: no signs of life"
Start the conversation!
// 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
}
}