Buy

Testing Exceptions

The 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!

9 lines src/AppBundle/Exception/NotABuffetException.php
... 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.

40 lines tests/AppBundle/Entity/EnclosureTest.php
... 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.

40 lines tests/AppBundle/Entity/EnclosureTest.php
... 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.

47 lines src/AppBundle/Entity/Enclosure.php
... 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!

47 lines src/AppBundle/Entity/Enclosure.php
... 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!

9 lines src/AppBundle/Exception/NotABuffetException.php
... lines 1 - 4
class NotABuffetException extends \Exception
{
... line 7
}

Now throw that exception!

47 lines src/AppBundle/Entity/Enclosure.php
... 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!

51 lines tests/AppBundle/Entity/EnclosureTest.php
... 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. Try it!

51 lines tests/AppBundle/Entity/EnclosureTest.php
... 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.

Leave a comment!