Testing Exceptions
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 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!
The Custom Exception Class
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)); | |
} | |
} |
Expecting an Exception
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!
expect Exceptions via Annotations
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.
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();