Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

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

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!

... 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.

Leave a comment!

8
Login or Register to join the conversation
CloudCreators Avatar
CloudCreators Avatar CloudCreators | posted 1 year ago

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();

Reply

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!

Reply
Marcel D. Avatar
Marcel D. Avatar Marcel D. | posted 3 years ago

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!

Reply

Hey Marcel dos Santos!

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!

2 Reply
Abelardo L. Avatar

Hi there,

Then you should delete the section which tells us about annotations to avoid to be followed by us.

Cheers!

Reply

Hey AbelardoLG,

Unfortunately, we cannot delete anything in released tutorials, but we will add a note to warn about it. Thank you!

Cheers!

Reply
Default user avatar
Default user avatar Boran Alsaleh | posted 4 years ago

Hallo ,

why we have to make sure adding $this->expectException(NotBuffetException::class); before our last code
$Enclosure->addDinosaur(new Dinosaur('Carn Dinasour',true));

thnx ,

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the fundamentals of PHPUnit haven't changed, this tutorial *is* built on an older version of Symfony and PHPUnit.

What PHP libraries does this tutorial use?

// 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
    }
}