It's been over a year since I've wrote the libellous words "One of the things the PHP community has always been crap about is testing"; an annum on and change is really starting to gain momentum. Personally, I'm now programatically testing pretty much every line of code I ship, in some way or another, and I've learnt a lot over the past year about the best ways to test. Others are starting to really see the value of unit testing.
One of the things I've discovered by talking to other developers is that a lot of people lack direction in what to test and how to make it a part of their process. They get the basics, but they struggle to get into the mindset of someone who writes tests. If you don't get it chances are you're doing it wrong.
Learning the tools is easy. Grabbing PHPUnit off PEAR is a piece of cake, as is setting up a couple of files and running phpunit at your command line. Nowadays, the basic steps of setting up an environment for testing are so few and so effortless, it's child's play. For that reason I don't believe there's much more value in writing another introductory article.
This piece is about the process of testing. It's about test-driven development, how I work when I'm writing code and how writing unit tests gives me smarter, more stable and more refactorable code.
Get PHPUnit installed. We're going to write a small PHP class that takes an email request body and parses a bunch of info about it.
Create two new files, email_parser_test.php and email_parser.php. I like to work with these files side by side, so in Sublime Text 2 - a great text editor which you should all be using, incidentally - I'll hit Command + Alt + 2 to open my editor up into two columns. This means I can have my tests on the left and my implementation on the right.
Inside our test file, create a new class and a basic test method:
class Email_parser_test extends PHPUnit_Framework_TestCase
{
public function test_something()
{
$this->assertTrue(true);
}
}
Jump into Terminal and give it a run:
$ phpunit --colors email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
.
Time: 0 seconds, Memory: 2.25Mb
OK (1 test, 1 assertion)
That --colors flag is going to get annoying, so we'll dump this in a phpunit.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true"></phpunit>
There's one tiny little bit more of setup we need to do before we can begin writing proper code. We'll need some test data to work with; an email we know the format of and can reproduce every time. Tests are only valuable if they're consistent. We want two emails to work with; and you'll see why in a moment. I'll use an email I got from good friend Alex Bilbie about his excellent CodeIgniter MongoDB Library. Save this in a file email.eml:
Delivered-To: jamie@jamierumbelow.net
Return-Path: <alex.bilbie@gmail.com>
Date: Wed, 2 May 2012 01:06:01 +0100
From: Alex Bilbie <alex.bilbie@gmail.com>
To: jamie@jamierumbelow.net
Message-ID: <586B7E1735704A1CB388F4434B4EAA9B@gmail.com>
Subject: =?utf8?Q?Update_to_my_Mongo_library?=
X-Mailer: sparrow 1.5 (build 1043.1)
Content-Type: text/html
MIME-Version: 1.0
I've updated my CodeIgniter MongoDB library to version 0.5.1. It mostly contains bug fixes and optimisations.
Version 2.0 of the library is almost finished – check it out at <a href="https://github.com/alexbilbie/codeigniter-mongodb-library/tree/v2">https://github.com/alexbilbie/codeigniter-mongodb-library/tree/v2</a>. I'm currently working out how PHP Unit works so that the library can be tested on Travis CI each time a new commit is made.
And another one (email2.eml):
Delivered-To: jamie@jamierumbelow.net
Return-Path: <me@jeremygimbel.net>
Date: Wed, 3 May 2012 09:25:55 +0100
From: Jeremy Gimbel <me@jeremygimbel.net>
To: praise@jamierumbelow.net
Message-ID: <109A8F7538567P7VD675E7990N8NDS7T@gmail.com>
Subject: =?utf8?Q?A_quick_note!?=
Content-Type: text/html
MIME-Version: 1.0
Jamie, I love what you're doing with <a href="https://github.com/jamierumbelow/codeigniter-base-model">MY_Model</a> and <a href="https://github.com/jamierumbelow/codeigniter-base-controller">MY_Controller</a>!
Now we know everything's working a okay and we've got our test data, we'll waste no more time and get straight on with writing our first test.
Actually, that was a lie. I just want to make three points before we begin:
- Remember that we should always write the smallest amount of code possible to get the test to pass. That'll become clearer shortly.
- We're writing unit tests, so we're testing the input -> output flow of a unit of code (and any side effects)
- Your tests should never negatively affect the API. If a parameter is only necessary for the tests, you're doing it wrong.
We'll start off by simply parsing the headers out and grabbing them. This is good place to start because we'll need to parse the headers anyway; it's therefore worth our while to test that core functionality first.
public function test_parses_headers()
{
$parsed = $this->parser->parse($this->email);
$this->assertEquals(10, count($parsed['headers']));
}
The test might be simple, and it certainly doesn't test all the functionality we need to, but that is exactly what we want.
When I test, I find it valuable to write the smallest amount of code to test and the smallest possible amount to get the test past. It doesn't matter what the system needs to do in the future, it's just about getting the test to past.
You'll spot that we're accessing $this->parser and $this->email. Let's define them:
public function setUp()
{
$this->parser = new Email_parser();
$this->email = file_get_contents('email.eml');
}
Run our tests:
$ phpunit email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
Configuration read from /Users/jamierumbelow/Sites/email_parser/phpunit.xml
PHP Fatal error: Class 'Email_parser' not found in /Users/jamierumbelow/Sites/email_parser/Email_parser_test.php on line 7
Fatal error: Class 'Email_parser' not found in /Users/jamierumbelow/Sites/email_parser/Email_parser_test.php on line 7
What's the smallest amount of code that can fix this error? In our Emailparser.php_ file:
class Email_parser { }
...and require it at the top of our test file:
require_once 'email_parser.php';
Run again and we get an error about missing parse() method. Define it:
public function parse($email) { }
Run again and we get an assertion error:
$ phpunit email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
Configuration read from /Users/jamierumbelow/Sites/email_parser/phpunit.xml
F
Time: 0 seconds, Memory: 2.75Mb
There was 1 failure:
1) Email_parser_test::test_parses_headers
Failed asserting that 0 matches expected 10.
/Users/jamierumbelow/Sites/email_parser/email_parser_test.php:17
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
What's the smallest amount of code that we can write to make this test pass?
return array( 'headers' => array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) );
You're probably shouting at the screen and telling me I'm an idiot right now, and honestly, I wouldn't blame you. This might seem like a total waste of time. You'll have to bare with me here; you'll soon see why this can become valuable.
Now we've got our test passing, let's add another assertion to it:
public function test_parses_headers()
{
$parsed = $this->parser->parse($this->email);
$this->assertEquals(10, count($parsed['headers']));
$this->assertEquals('jamie@jamierumbelow.net', $parsed['headers']['To']);
}
Got it yet? Nope? Okay, I'll carry on.
$ phpunit email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
Configuration read from /Users/jamierumbelow/Sites/email_parser/phpunit.xml
E
Time: 0 seconds, Memory: 2.50Mb
There was 1 error:
1) Email_parser_test::test_parses_headers
Undefined index: To
/Users/jamierumbelow/Sites/email_parser/email_parser_test.php:18
FAILURES!
Tests: 1, Assertions: 1, Errors: 1.
What's the smallest possible amount of code to fix it? We'll actually cheat a little bit here since we KNOW that once the To index is in the array, it'll ask for the value of jamie@jamierumbelow.net, so we might as well kill two birds with one stone.
return array( 'headers' => array( 'To' => 'jamie@jamierumbelow.net', 2, 3, 4, 5, 6, 7, 8, 9, 10 ) );
Good good. Let's add one more just to ensure we're being thorough:
$this->assertEquals('<586B7E1735704A1CB388F4434B4EAA9B@gmail.com>', $parsed['headers']['Message-ID']);
The failure:
1) Email_parser_test::test_parses_headers
Undefined index: Message-ID
Again, killing two birds with one stone:
return array(
'headers' => array( 'To' => 'jamie@jamierumbelow.net',
'Message-ID' => '<586B7E1735704A1CB388F4434B4EAA9B@gmail.com>',
3, 4, 5, 6, 7, 8, 9, 10 ) );
So WHAT IS THE POINT OF ALL THIS?
What happens when we copy + paste our assertions and run the same test on email2.eml as well?
Update the assertions so that we're checking for the appropriate number of headers and the appropriate values:
public function test_parses_headers()
{
$parsed = $this->parser->parse($this->email);
$this->assertEquals(10, count($parsed['headers']));
$this->assertEquals('jamie@jamierumbelow.net', $parsed['headers']['To']);
$this->assertEquals('<586B7E1735704A1CB388F4434B4EAA9B@gmail.com>', $parsed['headers']['Message-ID']);
$parsed = $this->parser->parse($this->email2);
$this->assertEquals(9, count($parsed['headers']));
$this->assertEquals('praise@jamierumbelow.net', $parsed['headers']['To']);
$this->assertEquals('<109A8F7538567P7VD675E7990N8NDS7T@gmail.com>', $parsed['headers']['Message-ID']);
}
And load our email in our tests' setUp():
$this->email2 = file_get_contents('email2.eml');
Run it:
$ phpunit email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
Configuration read from /Users/jamierumbelow/Sites/email_parser/phpunit.xml
F
Time: 0 seconds, Memory: 2.75Mb
There was 1 failure:
1) Email_parser_test::test_parses_headers
Failed asserting that 10 matches expected 9.
/Users/jamierumbelow/Sites/email_parser/email_parser_test.php:24
FAILURES!
Tests: 1, Assertions: 4, Failures: 1.
Understand the logic yet?
Ahhhhhh. There we go.
Suddenly, we're now in the position where in order to get this test to pass, we're forced to parse the email. In order to get the test passing, we need to implement our test appropriately. Adding in some weird switch as a parameter, although would require less code, detracts from our API (that is to say, it would be pointless in production/implementation code). So the only thing we can really do is actually parse the email.
public function parse($email)
{
list($headers_list, $body) = explode(PHP_EOL . PHP_EOL, $email, 2);
$headers_list = explode(PHP_EOL, $headers_list);
$headers = array();
foreach ($headers_list as $h)
{
list($key, $value) = explode(':', $h, 2);
$headers[$key] = trim($value);
}
return array( 'headers' => $headers );
}
This does two great things. It makes our tests pass:
$ phpunit email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
Configuration read from /Users/jamierumbelow/Sites/email_parser/phpunit.xml
.
Time: 0 seconds, Memory: 2.50Mb
OK (1 test, 6 assertions)
...and it has the fortunate side effect of providing us with the body, parsed and separated out nice and easily!
While it's good that that's already done for us, it's currently untested, so let's fix that posthaste:
public function test_parses_body()
{
$expected_body_email = "I've updated my CodeIgniter MongoDB library to version 0.5.1. It mostly contains bug fixes and optimisations.\n\nVersion 2.0 of the library is almost finished – check it out at <a href=\"https://github.com/alexbilbie/codeigniter-mongodb-library/tree/v2\">https://github.com/alexbilbie/codeigniter-mongodb-library/tree/v2</a>. I'm currently working out how PHP Unit works so that the library can be tested on Travis CI each time a new commit is made.";
$expected_body_email2 = 'Jamie, I love what you\'re doing with <a href="https://github.com/jamierumbelow/codeigniter-base-model">MY_Model</a> and <a href="https://github.com/jamierumbelow/codeigniter-base-controller">MY_Controller</a>!';
$parsed = $this->parser->parse($this->email);
$this->assertEquals($expected_body_email, $parsed['body']);
$parsed = $this->parser->parse($this->email2);
$this->assertEquals($expected_body_email2, $parsed['body']);
}
And making this pass is now just a matter of adding the $body variable to the return line:
return array( 'headers' => $headers, 'body' => $body );
So. We have our headers and we have our body. While cool, it's not very useful. Let's try to extract the subject.
For those of you who know the MIME protocol, you'll know that the subject field is usually encoded in some way. This is conventionally Q-encoding or Base64. PHP's iconv extension provides a great little function for decoding MIME headers: iconv_mime_decode.
Let's write a test:
public function test_subject()
{
$parsed = $this->parser->parse($this->email);
$this->assertEquals('Update to my Mongo library', $parsed['subject']);
}
To get it passing:
return array( 'headers' => $headers, 'body' => $body, 'subject' => 'Update to my Mongo library' );
Expand the tests:
$parsed = $this->parser->parse($this->email2);
$this->assertEquals('A quick note!', $parsed['subject']);
And we can implement the function to get the tests to pass:
$subject = iconv_mime_decode($headers['Subject']);
return array( 'headers' => $headers, 'body' => $body, 'subject' => $subject );
And there we have it!
$ phpunit email_parser_test.php
PHPUnit 3.6.10 by Sebastian Bergmann.
Configuration read from /Users/jamierumbelow/Sites/email_parser/phpunit.xml
...
Time: 0 seconds, Memory: 2.50Mb
OK (3 tests, 10 assertions)
It's a simple piece of code and a simple test suite, but hopefully this article has clarified how I test and why I believe this process to be truly valuable.