PHPUnit Starter
PHPUnit Starter
Elnur Abdurrakhimov
Buy on Leanpub

Introduction

PHPUnit is the defacto testing framework for PHP. If you want to start automating tests of your code, PHPUnit is the right tool for the job.

Why Automate Tests

Manual testing is an error prone and ineffective process.

Testing the same things over and over can bore anyone to death. And since that’s so boring, the natural tendency is to stop paying attention to details and miss bugs. Humans are not meant to do repetitive and mindless work; they’re meant to enjoy fun and creative activities like programming machines to do repetitive and mindless work for them.

And the good news is that while machines are dumb, they’re very fast at doing what they’ve been programmed to. So it makes perfect sense to teach them how to test our code and let them do that whenever we need them to.

Types of Tests

There are a lot of types of automated tests:

  • Unit,
  • Component,
  • Integration,
  • Functional,
  • UI/API,
  • End-to-end,
  • and so on.

Even though on a project with heavily automated testing you’ll meet test of most of those categories, unit tests are the easiest to start with and that’s what we’ll focus on in this book. After you get familiar with PHPUnit and unit tests, you’ll be able to use it for other types of tests, too.

Who Is This Book For

I assume you’re familiar with PHP. You don’t have to be an expert; knowing basics of the language and having experience with actually writing PHP code is enough.

I don’t assume you know anything about automating tests and hence I’ll introduce you to whatever you need to know to write tests with PHPUnit.

How You Should Read This Book

This book is written in a tutorial style and each chapter flows into the next one. Hence you should read the book from cover to cover and not skip chapters.

Besides, the book is so short that there should be no reasons to jump ahead. The whole book can be read in a day or two.

Sample Project

Since this is not an abstract book, we’ll be writing actual code. And to do that, we need a sample project to focus on.

I don’t want to bother you with learning an unfamiliar domain just to be able to read this book, so the project has to be simple. But in order to have enough problems to automate tests for to show you the features of PHPUnit you’re very likely to use, it has to be not too simple.

Taking all that into account, I came up with an idea to write a simple email address checker subsystem.

Imagine you’re working on some kind of a social network where people sign up with an email address and a password. One of the system requirements is to not let people sign up with an email with certain characteristics. For instance, an email ending with @example.com is invalid because example.com is a reserved domain and no one actually uses it.

You’ve been tasked with creating such a blacklist email address subsystem and that’s what we’ll be doing in this book. It won’t be sophisticated enough for real use, but it will be enough for you to learn PHPUnit.

But first let’s install PHPUnit and ensure it works.

Installation

The easiest way to install PHPUnit — or any other PHP library or framework for that matter — is to use Composer. Let’s install Composer first.

Installing Composer

If you’re using Linux/Unix/OSX, the installation process is simple. All you have to do is to run the following command in a terminal:

$ curl -sS https://getcomposer.org/installer | php
$ mv composer.phar /usr/local/bin/composer

To test that Composer has been installed successfully, run the following command:

$ composer -V

If everything went well, you should see output similar to this:

Composer version 1.0-dev (feefd5...54d0d5) 2015-12-03 16:17:58

If you’re getting that output, you can move on to the next section.

If you’re not getting that output or using another OS, please go through the official installation instructions.

Configuring the Project and Installing PHPUnit

Now that we have Composer installed, let’s configure our project and install PHPUnit.

Create a directory for the sample project we’re going to work throughout the book. I called it phpunit-starter in my system, but you can come up with any other name if you don’t like that one.

Now, create a file called composer.json in that directory with the following contents:

 1 {
 2     "require": {
 3         "phpunit/phpunit": "^4.8"
 4     },
 5     "autoload": {
 6         "psr-4": {
 7             "": "src/"
 8         }
 9     },
10     "config": {
11         "bin-dir": "bin"
12     }
13 }

This file tells Composer how to handle our application. Let’s go through it.

On lines 2-4, we have a require section. This section tells Composer which packages our application needs — project dependencies. The only dependency we have here is PHPUnit that we specify on line 3. ^4.8 is a version constraint that tells Composer to install PHPUnit of version 4.8 and up to but not including version 5.

On lines 5-9, we configure autoloading of PHP classes. We need that to avoid including all the files we need with include()/include_once()/require()/require_once() statements. Thanks to autoloading, classes will be found and loaded automatically.

On lines 10-12, we tell Composer to copy binary files that a dependency our project depends on can ship with. PHPUnit does ship with one. We need that to get the phpunit binary installed into the bin/ directory instead of the vendor/bin. We do that just for simplicity of executing PHPUnit.

Now that we have composer.json, let’s tell Composer to do its job:

$ composer install

If everything went as expected, you should see output similar to this:

Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing sebastian/version (1.0.6)

... omitted ...

  - Installing phpunit/phpunit (4.8.19)
  
... omitted ...

Generating autoload files

Now, several things should appear in the project directory. The vendor/ directory contains the dependency we specified in our composer.json file along with dependencies of that dependency and dependencies of those dependencies — you get the idea. It also includes autoloading-related files generated by Composer.

composer.lock is the file listing exact versions of installed dependencies. It makes sense to commit it to your project’s VCS to ensure that all team members and servers get exactly the same versions of dependencies to avoid hard-to-debug bugs caused by installing a dependency of a different version on some server. We don’t really care about that file in this book.

The last but far from the least thing that appeared in the project directory is the bin/ folder. It appeared here thanks to the configuration we did in our composer.json. It’s the most important thing that Composer added for us because it contains the phpunit executable.

Now, let’s verify that PHPUnit got installed correctly by running the following:

$ bin/phpunit --version

If everything went well, you should get output similar to this:

PHPUnit 4.8.19 by Sebastian Bergmann and contributors.

That line means that we got PHPUnit installed and are ready to start learning it. It’s time to write our first test.

The First Test

Remember our sample project? We need a system that prevents users from signing up with email addresses having particular characteristics. One of those characteristics is the use of a domain like example.com. So the first thing we need is to extract the domain part from an email address. Let’s start with a test for a class that will extract parts of an email address.

Writing the Test

We’re going to start with a test instead of writing the actual code and then adding tests for it. A benefit of that approach is to be able to figure out the most convenient way to use our class as a client. This leads to better code because if it’s hard to use a class in tests, it’s going to be hard to use for clients as well.

Another benefit of writing tests first is to define the boundaries of what our “real” code is supposed to do. If all tests pass, we’re either done or have to add more tests.

We will come back to these and other benefits of writing tests first later — after you’ve had enough of instant gratification of writing a couple of tests and making them pass. For now, let’s get to work.

We need a directory to put our tests into. Let’s create a directory called tests in the directory of the project.

Now, let’s create the file for the test class we’re going to write. Let’s call it EmailAddressPartsExtractorTest.php.

Finally, let’s write the test itself:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     public function testExtractDomain()
 7     {
 8         $extractor = new EmailAddressPartsExtractor();
 9         $this->assertEquals(
10             "example.com",
11             $extractor->extractDomain("elnur@example.com")
12         );
13     }
14 }

On line 3, we define the EmailAddressPartsExtractorTest test class that extends PHPUnit_Framework_TestCase. PHPUnit_Framework_TestCase is the class all PHPUnit test classes need to extend in order to be treated as such by PHPUnit. If we didn’t, PHPUnit would just skip that class instead of looking for test methods in it.

On line 6, we define the testExtractDomain() test method. PHPUnit knows that it’s a test method because of the test prefix. Not all methods in a test class have to be test methods. If you define a method without the test prefix, PHPUnit will just skip it.

On line 8, we create the $extractor instance of the EmailAddressPartsExtractor class. Don’t worry that the class doesn’t exist yet; we’ll get to it soon.

On lines 9-12, we call the assertEquals() method that comes with the PHPUnit_Framework_TestCase class that our test class extends. We pass two arguments to it: the expected result and the actual result of calling the extractDomain() method of our not yet existing class. According to the name of the method, it’s supposed to extract the domain part of an email address passed to it.

Let’s now run the test and see how it goes:

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

PHP Fatal error:  Class 'EmailAddressPartsExtractor' not found in /home/elnu\
r/proj/phpunit-starter/tests/EmailAddressPartsExtractorTest.php on line 8

It failed as expected. Let’s fix the problem by defining the missing class.

Making the Test Pass

Before we add the missing class, we need to create a directory for the “real” code of our application. Remember the autoload section from we added to the composer.json file? It points to the src directory of our project. Let’s create that directory then.

Now that we have the directory, let’s create the file that will hold the class. Let’s name it EmailAddressPartsExtractor.php and fill it with the following code:

<?php

class EmailAddressPartsExtractor
{
}

As you can see, that’s not much. But let’s rerun our test again:

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

PHP Fatal error:  Call to undefined method EmailAddressPartsExtractor::extra\
ctDomain() in /home/elnur/proj/phpunit-starter/tests/EmailAddressPartsExtrac\
torTest.php on line 11

We solved the previous problem but ran into another one — the missing method. Let’s add it:

<?php

class EmailAddressPartsExtractor
{
    public function extractDomain($emailAddress)
    {
    }
}

And let’s rerun the test again:

 1 $ bin/phpunit tests
 2 PHPUnit 4.8.19 by Sebastian Bergmann and contributors.
 3 
 4 F
 5 
 6 Time: 38 ms, Memory: 4.00Mb
 7 
 8 There was 1 failure:
 9 
10 1) EmailAddressPartsExtractorTest::testExtractDomain
11 Failed asserting that null matches expected 'example.com'.
12 
13 /home/elnur/proj/phpunit-starter/tests/EmailAddressPartsExtractorTest.php:12
14 
15 FAILURES!
16 Tests: 1, Assertions: 1, Failures: 1.

That’s much better. Now, instead of PHP itself crashing with fatal errors because of an undefined class or method, we get PHP executing properly and instead see our PHPUnit assertion fail. As you can see, we get a completely different output in this case. Let’s go through it.

On line 4, we see an F that stands for “failure”. We get a single character for each test we execute. Since we only have one test now, all we see is a single character. We’ll see more when we add more tests.

On line 6, PHPUnit reports how much time it took to run all tests and how much RAM was used by PHP.

Line 8 is self-explanatory.

On lines 10-11, PHPUnit tells us about the failed assertion. First it tells us which test has failed and then reports the reason it failed. That reason is specific to each assertion method. In our example, we use assertEquals() that expects the expected and actual values to match. Since our extractDomain() method doesn’t have any code in it, it returns null. And that’s what the assertion failure tells us about.

On line 13, PHPUnit reports the test class and the exact line the of the failed assertion. Since we split the assertion over 4 lines of code, it points to the last line of that assertEquals() method call.

On line 16, PHPUnit tells us about the total number of tests methods run, the total number of assertions executed, and the total number of failed assertions. Since a test method can have multiple assertions in it, that makes sense.

What’s left now is to make the test pass. Let’s do that now:

<?php

class EmailAddressPartsExtractor
{
    public function extractDomain($emailAddress)
    {
        return 'example.com';
    }
}
1 $ bin/phpunit --color tests
2 PHPUnit 4.8.19 by Sebastian Bergmann and contributors.
3 
4 .
5 
6 Time: 37 ms, Memory: 3.75Mb
7 
8 OK (1 test, 1 assertion)

Yay! It’s so much nicer to see green instead of red. Our first test is passing. And the output is much shorter this time. That’s because there are no failures for PHPUnit to report about.

The F on the line 4 got replaced with a dot. A dot means a passing test.

And the last 8th line says OK and reports how many test methods and assertions were executed. Since there are no failures here, PHPUnit doesn’t mention them.

But wait! Something is definitely wrong with our domain extracting implementation. Instead of writing some real logic, we just hardcoded the expected result. Isn’t that cheating?

Well, not really. I mean, we made the test pass, right? Who cares how exactly we did that? Oh, you do? Okay. Scroll to the next chapter and we’ll deal with that there.

Forcing Proper Implementation

We made our first test pass by hardcoding the return value:

<?php

class EmailAddressPartsExtractor
{
    public function extractDomain($emailAddress)
    {
        return 'example.com';
    }
}

Even though some people might think that’s cheating, that’s not the motivation here. The real motivation is to ensure that we do not write code that’s not covered by tests. Why do that? Because if we don’t, what’s the point of writing tests in the first place?

The main reason for writing tests is to get confidence in our code. And to get that confidence, we should only write the “real” code when there’s a failing test. Otherwise we end up with untested code.

Going back to our domain extraction code, we need to write enough tests or assertions to force us to write the actual implementation. Let’s do that now.

Triangulation

In testing parlance, triangulation is a process of adding more tests or assertions to force us into writing the real implementation of some logic. Here’s how we’re going to do that:

<?php

class EmailAddressPartsExtractorTest
    extends PHPUnit_Framework_TestCase
{
    public function testExtractDomain()
    {
        $extractor = new EmailAddressPartsExtractor();
        $this->assertEquals(
            "example.com",
            $extractor->extractDomain("elnur@example.com")
        );
        $this->assertEquals(
            "example.org",
            $extractor->extractDomain("elnur@example.org")
        );
        $this->assertEquals(
            "example.net",
            $extractor->extractDomain("elnur@example.net")
        );
    }
}

Notice that we added two more assertions with example.org and example.net domains as expected values of extracting the domain part of the elnur@example.org and elnur@example.net email addresses, respectively.

Let’s run our new test now:

 1 $ bin/phpunit --color tests
 2 PHPUnit 4.8.19 by Sebastian Bergmann and contributors.
 3 
 4 F
 5 
 6 Time: 40 ms, Memory: 4.00Mb
 7 
 8 There was 1 failure:
 9 
10 1) EmailAddressPartsExtractorTest::testExtractDomain
11 Failed asserting that two strings are equal.
12 --- Expected
13 +++ Actual
14 @@ @@
15 -'example.org'
16 +'example.com'
17 
18 /home/elnur/proj/phpunit-starter/tests/EmailAddressPartsExtractorTest.php:16
19 
20 FAILURES!
21 Tests: 1, Assertions: 2, Failures: 1.

Nice. Adding more assertions made our hardcoded logic to fail. Let’s go through the PHPUnit output.

On line 4, the dot got replaced with an F again.

On line 11, we now get a different assertion failure. That’s because this time the types of the expected and actual values match, while in the previous failure we were trying to compare null to a string. But even though the types match, the values don’t — and hence we get the failure.

We also get some new output on lines 12-16. That’s a diff output. The lines 12-13 show the legend to tell us that lines starting with minus signs are what was expected and lines starting with plus signs are the actual values. And that’s what we see on lines 15-16. Instead of the expected example.org string, we got the actual example.com string.

The last 22th line reports that 2 assertions got executed instead of 1. Why 2 and not 3? We have 3 assertions in our test, right? That’s because when an assertion fails, the test gets marked as failed and PHPUnit goes to the next test instead of executing the current test to the end.

Making the Test Pass Again

Let’s now try and get back to the nice green state of our test. We could come up with yet another hardcoded solution to make the test pass again:

<?php

class EmailAddressPartsExtractor
{
    public function extractDomain($emailAddress)
    {
        if (false !== strpos($emailAddress, 'example.com')) {
            return 'example.com';
        } elseif (false !== strpos($emailAddress, 'example.org')) {
            return 'example.org';
        } else {
            return 'example.net';
        }
    }
}

That would work, but that’s too much code. We could now go and add even more assertions to the test and that would make our test fail again. Then we’d have to add even more conditions to our hardcoded conditional logic, and so on.

The idea is to write the less amount of code to make tests pass — not more. So why bother with all that hardcoding when the actual implementation is much easier to write and requires much less code? Let’s do it properly now:

<?php

class EmailAddressPartsExtractor
{
    public function extractDomain($emailAddress)
    {
        return explode('@', $emailAddress)[1];
    }
}

All we do here is explode the passed string by the @ symbol and return the second element of the resulting array.

That’s it. That’s the actual implementation. It’s just a single line of code compared to the conditional logic showed above.

Let’s now run the test and see how it goes:

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

.

Time: 39 ms, Memory: 3.75Mb

OK (1 test, 3 assertions)

The F got replaced with a dot again. And the last line reports that all 3 assertions have been executed this time. Nice.

By adding more assertions, we’ve forced ourselves to provide the real implementation. The only problem here is that we’ve introduced duplication to the test method while doing that. That’s what we’re going to deal with next.

Reducing Duplication

We now have 3 assertions in our test. Notice that all 3 of them do the same thing. They all use the same assertEquals() assertion, supply an expected domain, and call the same method with an email address. The only difference is the email address and the expected domain. Everything else is duplication.

Extracting a Method

As programmers, we’ve been taught about the DRY principle and trained to eliminate duplication. While it’s a good practice to eliminate duplication in the “real” code, it’s not always a good idea to do that in tests. For instance, our first instinct might be to extract a method:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     public function testExtractDomain()
 7     {
 8         $extractor = new EmailAddressPartsExtractor();
 9         $this->assertDomainExtraction(
10             $extractor,
11             'example.com',
12             'elnur@example.com'
13         );
14         $this->assertDomainExtraction(
15             $extractor,
16             'example.org',
17             'elnur@example.org'
18         );
19         $this->assertDomainExtraction(
20             $extractor,
21             'example.net',
22             'elnur@example.net'
23         );
24     }
25 
26     private function assertDomainExtraction(
27         EmailAddressPartsExtractor $extractor,
28         $domain,
29         $emailAddress
30     ) {
31         $this->assertEquals(
32             $domain,
33             $extractor->extractDomain($emailAddress)
34         );
35     }
36 }

Here, on lines 26-35, we define the assertDomainExtraction() method and move the assertion to it. We define it as a private method because it’s for the internal use of the class and is not meant to be called from the outside.

Then, in the testExtractDomain() method, we replaced all 3 assertions with calls to that method.

While we have reduced some duplication by making our test calling a single method for each assertion instead of calling two methods, the test itself became less readable.

The problem is that just by reading the test method, it’s not really clear how the assertion is being done. We have to jump to another method to see that.

In contrast to “real” code, this sort of duplication elimination is actually a bad practice when it comes to tests because readability is more important here than perfect code factoring.

Extracting Data

Let’s solve the duplication problem differently. Instead of extracting a method, let’s extract the data our assertions use:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     public function testExtractDomain()
 7     {
 8         $extractor = new EmailAddressPartsExtractor();
 9 
10         $emailsToDomains = [
11             'elnur@example.com' => 'example.com',
12             'elnur@example.org' => 'example.org',
13             'elnur@example.net' => 'example.net',
14         ];
15 
16         foreach ($emailsToDomains as $email => $domain) {
17             $this->assertEquals(
18                 $domain,
19                 $extractor->extractDomain($email)
20             );
21         }
22     }
23 }

On lines 10-14 we create an associative array with email address used as keys and the expected domains as values.

Then we use the foreach loop on lines 16-21 to iterate over the array and assert the domain extracting implementation for each key/value pair.

This way, we reduced duplication but kept the assertion in the test method. We don’t have to jump to other methods to understand what’s going on now.

Conveniently, PHPUnit supports this pattern of data extraction natively. Let’s start using it.

Data Providers

Extracting sets of data to reduce duplication in tests is so common that PHPUnit supports it natively. The feature is called Data Providers. Let’s see how our test class is going to look when using that feature:

 1 <?php
 2 
 3 class EmailAddressPartsExtractorTest
 4     extends PHPUnit_Framework_TestCase
 5 {
 6     /**
 7      * @dataProvider emailAddressesToDomains
 8      */
 9     public function testExtractDomain($emailAddress, $domain)
10     {
11         $extractor = new EmailAddressPartsExtractor();
12 
13         $this->assertEquals(
14             $domain,
15             $extractor->extractDomain($emailAddress)
16         );
17     }
18 
19     public function emailAddressesToDomains()
20     {
21         return [
22             ['elnur@example.com', 'example.com'],
23             ['elnur@example.org', 'example.org'],
24             ['elnur@example.net', 'example.net'],
25         ];
26     }
27 }

On lines 19-26, we define the emailAddressesToDomains() method that returns an array of arrays. Notice that this time it’s not an array of key/value pairs in the form of key => value, but a multidimensional array. That’s the format PHPUnit requires because there can be more than two values in each item. For instance, it could be in the form of:

['elnur@example.com', 'elnur', 'example.com']

Also notice that the method is public. That’s because it’s going to be called from the outside by PHPUnit.

On lines 6-8, we define a PHPDoc block with the @dataProvider annotation followed by the name of the data providing method emailAddressesToDomains. That’s how we link a test method to a data providing method.

On line 9, we add two parameters to our test method’s signature — $emailAddress and $domain. That’s how the data from the data provider will get into our test method. The order of the parameters is directly related to the order of values in each item of the array returned from the data provider. Since we put the email address first and the domain second, that’s the order we have to define the test method’s parameters in.

And finally, on lines 13-16, we have our assertEquals() assertion now using the $emailAddress and $domain parameters.

Let’s rerun the test now:

1 $ bin/phpunit --color tests
2 PHPUnit 4.8.19 by Sebastian Bergmann and contributors.
3 
4 ...
5 
6 Time: 48 ms, Memory: 3.75Mb
7 
8 OK (3 tests, 3 assertions)

Yay. The test is passing. Or tests?

Notice that this time we got three dots instead of one. That’s because basically PHPUnit treats each combination of a test and an item from a data provider as a separate test.

Now that we’ve dealt with reducing duplication in our test, let’s make our domain extraction code more solid by properly handling situations when malformed email addresses get passed to it.

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 a reminder of what exactly is being tested when a new programmer joins a 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 nothing to them. 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 or another exception does, 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 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 validation logic by throwing different types of malformed emails at it. I’m leaving that to you as an exercise.