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 SubscribeThe dinosaur enclosures are looking great. There's just one minor problem: people keep accidentally putting nice, gentle veggie-eating dinosaurs into the same enclosure as flesh-eating carnivores. The result is... well... expensive, and there's a lot of cleanup too.
We need to prevent the meat eaters and the veggie eaters from being mixed inside the same enclosure. In other words, if somebody tries to do this, we need to throw an exception!
It's optional, but let's create a custom exception class for this: NotABuffetException
. I'll even give this a default message: people need to understand how horrible this is!
... lines 1 - 2 | |
namespace AppBundle\Exception; | |
... line 4 | |
class NotABuffetException extends \Exception | |
{ | |
protected $message = 'Please do not mix the carnivorous and non-carnivorous dinosaurs. It will be a massacre!'; | |
} |
Making sure that this exception is thrown at the right time is critical to business. So let's write a test: testItDoesNotAllowCarnivorousDinosaursToMixWithHerbivores
.
Inside the method, create this terrifying situation: $enclosure = new Enclosure()
and $enclosure->addDinosaur(new Dinosaur())
. By default, dinosaurs are non-carnivorous. So now, let's add a predator: new Dinosaur('Velociraptor')
and true
for the isCarnivorous
argument.
... lines 1 - 9 | |
class EnclosureTest extends TestCase | |
{ | |
... lines 12 - 28 | |
public function testItDoesNotAllowCarnivorousDinosToMixWithHerbivores() | |
{ | |
$enclosure = new Enclosure(); | |
$enclosure->addDinosaur(new Dinosaur()); | |
... lines 34 - 36 | |
$enclosure->addDinosaur(new Dinosaur('Velociraptor', true)); | |
} | |
} |
At this point, an exception should be thrown. So... how can we test for that? By telling PHPUnit to expect an exception with... well... $this->expectException()
and then the exception class: NotABuffetException::class
. Make sure you add this before calling the final code.
... lines 1 - 9 | |
class EnclosureTest extends TestCase | |
{ | |
... lines 12 - 28 | |
public function testItDoesNotAllowCarnivorousDinosToMixWithHerbivores() | |
{ | |
... lines 31 - 34 | |
$this->expectException(NotABuffetException::class); | |
... lines 36 - 37 | |
} | |
} |
If we've done our work correctly, this should fail. Try the test!
./vendor/bin/phpunit
Yes! Failed asserting that exception of type NotABuffetException
is thrown.
Awesome! Let's go throw that exception! Inside Enclosure
, at the bottom, add a new private function
called canAddDinosaur
with a Dinosaur
argument. This will return a bool
.
... lines 1 - 13 | |
class Enclosure | |
{ | |
... lines 16 - 40 | |
private function canAddDinosaur(Dinosaur $dinosaur): bool | |
{ | |
... lines 43 - 44 | |
} | |
} |
Here's some simple logic: return count($this->dinosaurs) === 0
. So, if the enclosure is empty, then it's definitely ok to add a dinosaur. Or, check to see if $this->dinosaurs->first()->isCarnivorous() === $dinosaur->isCarnivorous()
. If they match, we're good!
... lines 1 - 13 | |
class Enclosure | |
{ | |
... lines 16 - 40 | |
private function canAddDinosaur(Dinosaur $dinosaur): bool | |
{ | |
return count($this->dinosaurs) === 0 | |
|| $this->dinosaurs->first()->isCarnivorous() === $dinosaur->isCarnivorous(); | |
} | |
} |
Back in addDinosaur()
, if not $this->canAddDinosaur()
. Throw the exception! Oh wait... make sure the class extends \Exception
. My bad!
... lines 1 - 4 | |
class NotABuffetException extends \Exception | |
{ | |
... line 7 | |
} |
Now throw that exception!
... lines 1 - 13 | |
class Enclosure | |
{ | |
... lines 16 - 31 | |
public function addDinosaur(Dinosaur $dinosaur) | |
{ | |
if (!$this->canAddDinosaur($dinosaur)) { | |
throw new NotABuffetException(); | |
} | |
... lines 37 - 38 | |
} | |
... lines 40 - 45 | |
} |
Check the tests!
./vendor/bin/phpunit
Woo! We got it!
There's one other way to test for exceptions. It's really the same, but looks fancier. Copy the test method and rename it so we can test for the opposite condition: testItDoesNotAllowToAddNonCarnivorousDinosaursToCarnivorousEnclosure
. Wow that's a long name!
... lines 1 - 9 | |
class EnclosureTest extends TestCase | |
{ | |
... lines 12 - 42 | |
public function testItDoesNotAllowToAddNonCarnivorousDinosaursToCarnivorousEnclosure() | |
{ | |
... lines 45 - 48 | |
} | |
} |
Add the Velociraptor first and then remove expectException()
. Instead, add an annotation: @expectedException
followed by the full class. PhpStorm puts the short name... so go copy the use statement and put it down here.
Tip
Actually, the @expectedException
, @expectedExceptionCode
, @expectedExceptionMessage
,
and @expectedExceptionMessageRegExp
annotations are deprecated and will be removed
in PHPUnit 9. Continue using expectException()
, expectExceptionCode()
, expectExceptionMessage()
,
and expectExceptionMessageRegExp()
methods accordingly in your PHP code.
Try it!
... lines 1 - 9 | |
class EnclosureTest extends TestCase | |
{ | |
... lines 12 - 39 | |
/** | |
* @expectedException \AppBundle\Exception\NotABuffetException | |
*/ | |
public function testItDoesNotAllowToAddNonCarnivorousDinosaursToCarnivorousEnclosure() | |
{ | |
$enclosure = new Enclosure(); | |
$enclosure->addDinosaur(new Dinosaur('Velociraptor', true)); | |
$enclosure->addDinosaur(new Dinosaur()); | |
} | |
} |
./vendor/bin/phpunit
Yes! One more test passing.
I want to go through one more example next... and also add some security to the enclosures. Our guests have been terrorized enough.
Hey Shubham,
The "dinosaurs" property is a Doctrine's ArrayCollection... but yeah, you can think about it like about an array on steroids, and the first() method returns the first value of that array or null if the array is empty, that's why we check if the array is NOT null first with "count($this->dinosaurs) === 0" check, and then we're sure that the first() will return a Dinosaur entity on which we can call that isCarnivorous() method and do not worry it will fail with "Method was called on null" error :)
Cheers!
Hi friends! For those who are using PHPUnit 8 the @expectedException
annotation is deprecated.
`
PHPUnit 8.0.4 by Sebastian Bergmann and contributors.
.......W.IS........ 19 / 19 (100%)
Time: 77 ms, Memory: 4.00 MB
There was 1 warning:
1) Tests\AppBundle\Entity\EnclosureTest::testItDoesNotAllowToAddNonCarnivorousDinosaursToCarnivorousEnclosure
The @expectedException, @expectedExceptionCode, @expectedExceptionMessage, and @expectedExceptionMessageRegExp annotations are deprecated. They will be removed in PHPUnit 9. Refactor your test to use expectException(), expectExceptionCode(), expectExceptionMessage(), or expectExceptionMessageRegExp() instead.
WARNINGS!
Tests: 19, Assertions: 31, Warnings: 1, Skipped: 1, Incomplete: 1.
`
Bye!
Hey Marcel D.!
Thanks for posting! You're totally right - you should now use the pure PHP way of expecting exceptions (the first we show) - but not the annotations :).
Cheers!
Hi there,
Then you should delete the section which tells us about annotations to avoid to be followed by us.
Cheers!
Hey AbelardoLG,
Unfortunately, we cannot delete anything in released tutorials, but we will add a note to warn about it. Thank you!
Cheers!
Hallo ,
why we have to make sure adding $this->expectException(NotBuffetException::class); before our last code
$Enclosure->addDinosaur(new Dinosaur('Carn Dinasour',true));
thnx ,
Hey Boran Alsaleh
Because when you add an expected exception like that, PHPUnit will wrap within a try-catch block your test method, so if it throws, it will check if the thrown exception matches to the one you specified, otherwise, the execution would explode (because you know, the code throwed an exception and nobody catched it)
I hope it helps you understanding what's going on here :)
Cheers!
// composer.json
{
"require": {
"php": "^7.0, <7.4",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/doctrine-bundle": "^1.6", // 1.10.3
"doctrine/orm": "^2.5", // v2.7.2
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"sensio/distribution-bundle": "^5.0.19", // v5.0.21
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.28
"symfony/monolog-bundle": "^3.1.0", // v3.1.2
"symfony/polyfill-apcu": "^1.0", // v1.6.0
"symfony/swiftmailer-bundle": "^2.3.10", // v2.6.7
"symfony/symfony": "3.3.*", // v3.3.13
"twig/twig": "^1.0||^2.0" // v2.4.4
},
"require-dev": {
"doctrine/data-fixtures": "^1.3", // 1.3.3
"doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
"liip/functional-test-bundle": "^1.8", // 1.8.0
"phpunit/phpunit": "^6.3", // 6.5.2
"sensio/generator-bundle": "^3.0", // v3.1.6
"symfony/phpunit-bridge": "^3.0" // v3.4.30
}
}
dinosaurs is an array of dinosaur, first() does what?
is it checking only the first dinosaur in the array?
return count($this->dinosaurs) === 0 || $this->dinosaurs->first()->isCarnivorous() === $dinosaur->isCarnivorous();