Handling Error Conditions

Up until now, we assumed that the extractDomain() method of our EmailAddressPartsExtractor class would always deal with a correct email address. That’s called a happy path or happy path scenario.

We know that it’s highly unlikely for any nontrivial code base to be used in happy path scenarios only. Programmers make mistakes. Users make mistakes. Things go wrong. The only question is what we program our code to do when things do go wrong.

Unhappy Path

Let’s write another test to see what happens when we supply a malformed email address:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     // ... omitted
 7 
 8     public function testExtractDomainWithMalformedEmailAddress()
 9     {
10         $extractor = new EmailAddressPartsExtractor();
11         $extractor->extractDomain('foo');
12     }
13 }

On line 8, we define a new test method. Its name testExtractDomainWithMalformedEmailAddress() clearly expresses what the test is about. Writing descriptive test method names is a good practice to follow. It serves as a reminder of what exactly is being tested when a new programmer joins the project or the original developer comes back to the test months later.

On line 10, we create a new instance of EmailAddressPartsExtractor. Nothing new here. Each test creates its own instance of a class it’s testing.

On line 11, we just call the same extractDomain() method we called before, but this time we pass foo as a malformed email address.

Let’s now run our tests to see what happens:

 1 $ bin/phpunit --color tests
 2 PHPUnit 4.8.19 by Sebastian Bergmann and contributors.
 3 
 4 ...E
 5 
 6 Time: 39 ms, Memory: 3.75Mb
 7 
 8 There was 1 error:
 9 
10 1) EmailAddressPartsExtractorTest::testExtractDomainWithMalformedEmailAddress
11 Undefined offset: 1
12 
13 /home/elnur/proj/phpunit-starter/src/EmailAddressPartsExtractor.php:7
14 /home/elnur/proj/phpunit-starter/tests/EmailAddressPartsExtractorTest.php:31
15 
16 FAILURES!
17 Tests: 4, Assertions: 3, Errors: 1.

A lot of new stuff here. Let’s go through it.

On line 4, our new test gets marked as E that stands for “error”. It’s not an F because it’s a PHP error and not an assertion failure. We don’t even have an assertion in our new test. All we get is a PHP error.

On line 10, we see the test class and the test method that caused that error to happen. Notice how the testExtractDomainWithMalformedEmailAddress method name part helps us quickly determine which test has failed. That’s why informative test method names are important.

On line 11, we see the exact PHP error that occurred. It tells us that we tried to access an array’s element with offset 1. Since there is no @ character in the passed string, explode() doesn’t split the address into 2 parts and we get an array with a single element foo. That’s why an attempt to access its second element fails.

And the last 17th line tells us that we now have 4 tests, 3 assertions have been executed, and we got 1 error. Note that it says Errors instead of Failures we’ve seen before when a PHPUnit assertion failed.

Our domain extraction code is supposed to fail when a malformed email address gets passed to it. But failing with a PHP error is not the best way to do it.

Failing Properly

Failing with a PHP error the way our method does now is far from ideal because clients of our EmailAddressPartsExtractor class have no clue how it works internally. The error message Undefined offset: 1 tells them nothing. They don’t even know that we’re using the explode() function and deal with an array it returns. To figure out what that error message means, a client of our class has to read its code. We can do better.

We should be proactive and not let that error happen in the first place. If a passed email address doesn’t have an @ character in it, we should not try and explode it by that character.

In object-oriented programming, the idiomatic way to fail early is to throw an exception. So let’s throw one.

First, let’s create an exception class with a name that tells exactly the reason that exception gets thrown. How about MalformedEmailAddressException? Looks pretty descriptive.

Let’s create the MalformedEmailAddressException.php file in the src folder and fill it with the following contents:

<?php

class MalformedEmailAddressException extends Exception
{
}

That’s it. All we need to create a custom exception class is to create a class that extends PHP’s Exception class. Let’s put it to use now.

First, let’s make our test expect the exception and fail itself if it doesn’t get thrown:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     // ... omitted
 7 
 8     public function testExtractDomainWithMalformedEmailAddress()
 9     {
10         $extractor = new EmailAddressPartsExtractor();
11 
12         try {
13             $extractor->extractDomain('foo');
14         } catch (MalformedEmailAddressException $expected) {
15             return;
16         }
17 
18         $this->fail('Expected MalformedEmailAddressException');
19     }
20 }

On lines 12-16, we wrap the extractDomain() method call with a try/catch block.

On line 13, we catch our newly created MalformedEmailAddressException exception. If it gets caught, we return from the method on line 14.

If the exception does not get thrown, we manually fail the test on line 18 by calling the fail() method with a descriptive message to help us make sense of test output when the test fails. The fail() method comes with the PHPUnit_Framework_TestCase parent class of our test class.

If another exception gets thrown, it will propagate up the call stack until it reaches PHPUnit and PHPUnit will output that exception. That’s because we don’t catch other exceptions in this test.

If we rerun our tests now, we’ll get the same output because the PHP error does not let our test to get to the last line.

Let’s first modify our domain extraction code to return an empty string to see our test failing for the right reason:

 1 <?php
 2 
 3 class EmailAddressPartsExtractor
 4 {
 5     public function extractDomain($emailAddress)
 6     {
 7         if (false === strpos($emailAddress, '@')) {
 8             return '';
 9         }
10 
11         return explode('@', $emailAddress)[1];
12     }
13 }

On line 7, we’ve added a check if the passed email address contains an @ character. If it doesn’t, we return an empty string on line 8.

If we rerun our tests now, we’ll get the following output:

 1 $ bin/phpunit --color tests
 2 PHPUnit 4.8.19 by Sebastian Bergmann and contributors.
 3 
 4 ...F
 5 
 6 Time: 47 ms, Memory: 3.75Mb
 7 
 8 There was 1 failure:
 9 
10 1) EmailAddressPartsExtractorTest::testExtractDomainWithMalformedEmailAddress
11 Expected MalformedEmailAddressException
12 
13 /home/elnur/proj/phpunit-starter/tests/EmailAddressPartsExtractorTest.php:38
14 
15 FAILURES!
16 Tests: 4, Assertions: 3, Failures: 1.

The E on line 4 got replaced with an F. That’s an improvement. Instead of having the test fail with a PHP error, we now get a PHPUnit failure caused by the call to the fail() method.

On line 11, we see the failure message we passed to the fail() method. That helps us figure out why the test failed. And we can see that it failed for the right reason.

Let’s now make our test pass by throwing the expected exception:

 1 <?php
 2 
 3 class EmailAddressPartsExtractor
 4 {
 5     public function extractDomain($emailAddress)
 6     {
 7         if (false === strpos($emailAddress, '@')) {
 8             throw new MalformedEmailAddressException();
 9         }
10 
11         return explode('@', $emailAddress)[1];
12     }
13 }

All we changed here is the 8th line. Instead of returning an empty string, we throw the expected exception now.

Rerunning tests tells us we did good:

$ bin/phpunit --color tests
PHPUnit 4.8.19 by Sebastian Bergmann and contributors.

....

Time: 38 ms, Memory: 3.75Mb

OK (4 tests, 3 assertions)

Nice. Let’s now make our exception expectation code cleaner.

PHPUnit’s Expected Exception Support

The way we’ve implemented exception expectation in our test is clunky. It’s too much boilerplate code for such a simple task as defining an expected exception. First you have to write that code, and then you and other programmers from your team have to read that bunch of lines of code to understand what the test actually does. Far from ideal.

Catching exceptions in tests is such a common problem that it’d be weird if PHPUnit didn’t have native support for it. And indeed it does. Let’s put it to use.

The first way to define an expected exception is to call the setExpectedException() PHPUnit’s method:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     // ... omitted
 7 
 8     public function testExtractDomainWithMalformedEmailAddress()
 9     {
10         $this->setExpectedException(MalformedEmailAddressException::class);
11 
12         $extractor = new EmailAddressPartsExtractor();
13         $extractor->extractDomain('foo');
14     }
15 }

We replaced the try/catch block and the fail() method call with a single method call on line 10. All we have to do is to call the setExpectedException() method and pass the class of the expected exception.

You can rerun tests now and see that you get the same output. But the test is much cleaner now.

The second approach — the one I prefer — is to use the @expectedException annotation:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     // ... omitted
 7 
 8     /**
 9      * @expectedException MalformedEmailAddressException
10      */
11     public function testExtractDomainWithMalformedEmailAddress()
12     {
13         $extractor = new EmailAddressPartsExtractor();
14         $extractor->extractDomain('foo');
15     }
16 }

On line 9, we’ve added the @expectedException annotation followed by the class name of the expected exception. That’s it.

Notice how much shorter the test has become. No more noise distracting us from the true purpose of the test.

If you make the extractDomain() method return an empty string instead of throwing an exception and rerun tests, you’ll that the error message is more informative when using PHPUnit’s native exception expectation declarations:

1) EmailAddressPartsExtractorTest::testExtractDomainWithMalformedEmailAddress
Failed asserting that exception of type "MalformedEmailAddressException" is \
thrown.

That’s definitely an improvement.

Now that you know how to handle error conditions with expected exceptions, you could try and combine that with the usage of a data provider to improve the email address validation logic by throwing different types of malformed email addresses at it. I’m leaving that to you as an exercise.

Our EmailAddressPartsExtractor is now feature-rich and solid enough to be used by other parts of code that will be doing the actual validation of email addresses. That’s what we’re going to work on in the next chapter. See you there.