In this tutorial, I will present an end-to-end example of a simple application - made strictly with TDD in PHP. I will walk you through each step, one at a time, while explaining the decisions I made in order to get the task done. The example closely follows the rules of TDD: write tests, write code, refactor.
Step 1 - Introduction to TDD & PHPUnit
Test Driven Development (TDD)
TDD is a "test-first" technique to develop and design software. It is almost always used in agile teams, being one of the core tools of agile software development. TDD was first defined and introduced to the professional community by Kent Beck in 2002. Since then, it has become an accepted - and recommended - technique in everyday programming.
TDD has three core rules:
- You are not allowed to write any production code, if there is not a failing test to warrant it.
- You are not allowed to write more of a unit test than is strictly necessary to make it fail. Not compiling / running is failing.
- You are not allowed to write more production code than is strictly necessary to make the failing test pass.
PHPUnit
PHPUnit is the tool that allows PHP programmers to perform unit testing, and practice test-driven development. It is a complete unit testing framework with mocking support. Even though there are a few alternative choices, PHPUnit is the most used and most complete solution for PHP today.
To install PHPUnit, you can either follow along with the previous tutorial in our "TDD in PHP" session, or you can use PEAR, as explained in the official documentation:
- become
root
or usesudo
- make sure you have the latest PEAR:
pear upgrade PEAR
- enable auto discovery:
pear config-set auto_discover 1
- install PHPUnit:
pear install pear.phpunit.de/PHPUnit
More information and instructions for installing extra PHPUnit modules can be found in the official documentation.
Some Linux distributions offer phpunit as a precompiled package, though I always recommend an installation, via PEAR, because it ensures that the most recent and up-to-date version is installed and used.
NetBeans & PHPUnit
If you're a fan of NetBeans, you can configure it to work with PHPUnit by following these steps:
- Go to NetBeans' configuration (Tools / Options)
- Select PHP / Unit Testing
- Check that the "PHPUnit Script" entry points to a valid PHPUnit executable. If it does not, NetBeans will tell you this, so if you don't see any red notices on the page, you are good to go. If not, look for the PHPUnit executable on your system and enter its path in the input field. For Linux systems, this path is typically /usr/bin/phpunit.
If you do not use an IDE with unit testing support, you can always run your test directly from the console:
cd /my/applications/test/folder phpunit
Step 2 - The Problem to Solve
Our team is tasked with the implementation of a "word wrap" feature.
Let's assume that we are part of a large corporation, which has a sophisticated application to develop and maintain. Our team is tasked with the implementation of a "word wrap" feature. Our clients don't wish to see horizontal scroll bars, and it's out job to comply.
In that case, we need to create a class that is capable of formatting an arbitrary bit of text provided as input. The result should be word wrapped at a specified number of characters. The rules of word wrapping should follow the behavior of other every-day applications, like text editors, web page text areas, etc. Our client does not understand all the rules of word wrapping, but they know they want it, and they know it should work in the same way that they've experienced in other apps.
Step 3 - Planning
TDD helps you achieve a better design, but it does not eliminate the need for up-front design and thinking.
One of the things that many programmers forget, after they start TDD, is to think and plan beforehand. TDD helps you achieve a better design most of the time, with less code and verified functionality, but it does not eliminate the need for up-front design and human thinking.
Every time you need to solve a problem, you should set aside time to think about it, to imagine a little design - nothing fancy - but enough to get you started. This part of the job also helps you to imagine and guess possible scenarios for the logic of the application.
Let's think about the basic rules for a word wrap feature. I suppose some un-wrapped text will be given to us. We will know the number of characters per line and we will want it to be wrapped. So, the first thing that comes to my mind is that, if the text has more characters than the number on one line, we should add a new line instead of the last space character that is still on the line.
Okay, that would sum up the behavior of the system, but it is much too complicated for any test. For example, what about when a single word is longer than the number of characters allowed on a line? Hmmm... this looks like an edge case; we can't replace a space with a new line since we have no spaces on that line. We should force wrap the word, effectively splitting it into two.
These ideas should be clear enough to the point that we can start programming. We'll need a project and a class. Let's call it Wrapper
.
Step 4 - Starting the Project and Creating the First Test
Let's create our project. There should be a main folder for source classes, and a Tests/
folder, naturally, for the tests.
The first file we will create is a test within the Tests
folder. All our future test will be contained within this folder, so I will not specify it explicitly again in this tutorial. Name the test class something descriptive, but simple. WrapperTest
will do for now; our first test looks something like this:
require_once dirname(__FILE__) . '/../Wrapper.php'; class WrapperTest extends PHPUnit_Framework_TestCase { function testCanCreateAWrapper() { $wrapper = new Wrapper(); } }
Remember! We are not allowed to write any production code before a failing test - not even a class declaration! That's why I wrote the first simple test above, called canCreateAWrapper
. Some consider this step useless, but I consider it to be a nice opportunity to think about the class we are going to create. Do we need a class? What should we call it? Should it be static?
When you run the test above, you will receive a Fatal Error message, like the following:
PHP Fatal error: require_once(): Failed opening required '/path/to/WordWrapPHP/Tests/../Wrapper.php' (include_path='.:/usr/share/php5:/usr/share/php') in /path/to/WordWrapPHP/Tests/WrapperTest.php on line 3
Yikes! We should do something about it. Create an empty Wrapper
class in the project's main folder.
class Wrapper {}
That's it. If you run the test again, it passes. Congratulations on your first test!
Step 5 - The First Real Test
So we have our project set up and running; now we need to think about our first real test.
What would be the simplest...the dumbest...the most basic test that would make our current production code fail? Well, the first thing that comes to mind is "Give it a short enough word, and expect the result to be unchanged." This sounds doable; let's write the test.
require_once dirname(__FILE__) . '/../Wrapper.php'; class WrapperTest extends PHPUnit_Framework_TestCase { function testDoesNotWrapAShorterThanMaxCharsWord() { $wrapper = new Wrapper(); assertEquals('word', $wrapper->wrap('word', 5)); } }
That looks fairly complicated. What does "MaxChars" in the function name mean? What does 5
in the wrap
method refer to?
I think something is not quite right here. Isn't there a simpler test that we can run? Yes, there certainly is! What if we wrap ... nothing - an empty string? That sounds good. Delete the complicated test above, and, instead, add our new, simpler one, shown below:
require_once dirname(__FILE__) . '/../Wrapper.php'; class WrapperTest extends PHPUnit_Framework_TestCase { function testItShouldWrapAnEmptyString() { $wrapper = new Wrapper(); $this->assertEquals('', $wrapper->wrap('')); } }
This is much better. The name of the test is easy to understand, we have no magic strings or numbers, and most of all, IT FAILS!
Fatal error: Call to undefined method Wrapper::wrap() in ...
As you can observe, I deleted our very first test. It is useless to explicitly check if an object can be initialized, when other tests also need it. This is normal. With time, you will find that deleting tests is a common thing. Tests, especially unit tests, have to run fast - really fast... and frequently - very frequently. Considering this, eliminating redundancy in tests is important. Imagine that you run thousands of tests every time you save the project. It should take no more than a couple of minutes, maximum, for them to run. So, don't be terrified to delete a test, if necessary.
Getting back to our production code, let's make that test pass:
class Wrapper { function wrap($text) { return; } }
Above, we've added absolutely no more code than is necessary to make the test pass.
Step 6 - Pressing On
Now, for the next failing test:
function testItDoesNotWrapAShortEnoughWord() { $wrapper = new Wrapper(); $this->assertEquals('word', $wrapper->wrap('word', 5)); }
Failure message:
Failed asserting that null matches expected 'word'.
And the code that makes it pass:
function wrap($text) { return $text; }
Wow! That was easy, wasn't it?
While we are in the green, observe that our test code can begin to rot. We need to refactor a few things. Remember: always refactor when your tests pass; this is the only way that you can be certain that you've refactored correctly.
First, let's remove the duplication of the initialization of the wrapper object. We can do this only once in the setUp()
method, and use it for both tests.
class WrapperTest extends PHPUnit_Framework_TestCase { private $wrapper; function setUp() { $this->wrapper = new Wrapper(); } function testItShouldWrapAnEmptyString() { $this->assertEquals('', $this->wrapper->wrap('')); } function testItDoesNotWrapAShortEnoughWord() { $this->assertEquals('word', $this->wrapper->wrap('word', 5)); } }
The
setup
method will run before each new test.
Next, there are some ambiguous bits in the second test. What is 'word'? What is '5'? Let's make it clear so that the next programmer who reads these tests doesn't have to guess.
Another programmer should be able to read the tests as easily as they would read the documentation.
function testItDoesNotWrapAShortEnoughWord() { $textToBeParsed = 'word'; $maxLineLength = 5; $this->assertEquals($textToBeParsed, $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
Now, read this assertion again. Doesn't that read better? Of course it does. Don't be afraid of lengthy variable names for your tests; auto-completion is your friend! It's better to be as descriptive as possible.
Now, for the next failing test:
function testItWrapsAWordLongerThanLineLength() { $textToBeParsed = 'alongword'; $maxLineLength = 5; $this->assertEquals("along\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
And the code that makes it pass:
function wrap($text, $lineLength) { if (strlen($text) > $lineLength) return substr ($text, 0, $lineLength) . "\n" . substr ($text, $lineLength); return $text; }
That's the obvious code to make our last test pass. But be careful - it is also the code that makes our first test to not pass!
We have two options to fix this problem:
- modify the code - make the second parameter optional
- modify the first test - and make it call the code with a parameter
If you choose the first option, making the parameter optional, that would present a little problem with the current code. An optional parameter is also initialized with a default value. What could such a value be? Zero might sound logical, but it would imply writing code just to treat that special case. Setting a very large number, so that the first if statement would not result in true can be another solution. But, what is that number? Is it 10? Is it 10000? Is it 10000000? We can't really say.
Considering all these, I will just modify the first test:
function testItShouldWrapAnEmptyString() { $this->assertEquals('', $this->wrapper->wrap('', 0)); }
Again, all green. We can now move on to the next test. Let's make sure that, if we have a very long word, it will wrap on several lines.
function testItWrapsAWordSeveralTimesIfItsTooLong() { $textToBeParsed = 'averyverylongword'; $maxLineLength = 5; $this->assertEquals("avery\nveryl\nongwo\nrd", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
This obviously fails, because our actual production code wraps only once.
Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ 'avery -veryl -ongwo -rd' +verylongword'
Can you smell the while
loop coming? Well, think again. Is a while
loop the simplest code that would make the test pass?
According to 'Transformation Priorities' (by Robert C. Martin), it is not. Recursion is always simpler than a loop and it is much more testable.
function wrap($text, $lineLength) { if (strlen($text) > $lineLength) return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); return $text; }
Can you even spot the change? It was a simple one. All we did was, instead of concatenating with the rest of the string, we concatenate with the return value of calling ourself with the rest of the string. Perfect!
Step 7 - Just Two Words
The next simplest test? What about two words can wrap, when there's a space at the end of the line.
function testItWrapsTwoWordsWhenSpaceAtTheEndOfLine() { $textToBeParsed = 'word word'; $maxLineLength = 5; $this->assertEquals("word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
That fits nicely. However, the solution may be getting a bit trickier this time.
At first, you might refer to a str_replace()
to get rid of the space and insert a new line. Don't; that road leads to a dead end.
The second most obvious choice would be an if
statement. Something like this:
function wrap($text, $lineLength) { if (strpos($text,' ') == $lineLength) return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength); if (strlen($text) > $lineLength) return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); return $text; }
However, that enters an endless loop, which will cause the tests to error out.
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted
This time, we need to think! The problem is that our first test has a text with a length of zero. Also, strpos()
returns false when it can't find the string. Comparing false with zero ... is? It is true
. This is bad for us because the loop will became infinite. The solution? Let's change the first condition. Instead of searching for a space and comparing its position with the line's length, let's instead attempt to directly take the character at the position indicated by the line's length. We will do a substr()
only one character long, starting at just the right spot in the text.
function wrap($text, $lineLength) { if (substr($text, $lineLength - 1, 1) == ' ') return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength); if (strlen($text) > $lineLength) return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); return $text; }
But, what if the space is not right at the end of line?
function testItWrapsTwoWordsWhenLineEndIsAfterFirstWord() { $textToBeParsed = 'word word'; $maxLineLength = 7; $this->assertEquals("word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
Hmm... we have to revise our conditions again. I am thinking that we will, after all, need that search for the position of the space character.
function wrap($text, $lineLength) { if (strlen($text) > $lineLength) { if (strpos(substr($text, 0, $lineLength), ' ') != 0) return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength); return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); } return $text; }
Wow! That actually works. We moved the first condition inside the second one so that we avoid the endless loop, and we added the search for space. Still, it looks rather ugly. Nested conditions? Yuck. It's time for some refactoring.
function wrap($text, $lineLength) { if (strlen($text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength), ' ') != 0) return substr ($text, 0, strpos($text, ' ')) . "\n" . $this->wrap(substr($text, strpos($text, ' ') + 1), $lineLength); return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); }
That's better better.
Step 8 - What About Multiple Words?
Nothing bad can happen as a result of writing a test.
The next simplest test would be to have three words wrapping on three lines. But that test passes. Should you write a test when you know it will pass? Most of the time, no. But, if you have doubts, or you can imagine obvious changes to the code that would make the new test fail and the others pass, then write it! Nothing bad can happen as a result of writing a test. Also, consider that your tests are your documentation. If your test represents an essential part of your logic, then write it!
Further, the fact the tests we came up with are passing is an indication that we are getting close to a solution. Obviously, when you have a working algorithm, any test that we write will pass.
Now - three words on two lines with the line ending inside the last word; now, that fails.
function testItWraps3WordsOn2Lines() { $textToBeParsed = 'word word word'; $maxLineLength = 12; $this->assertEquals("word word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
I nearly expected this one to work. When we investigate the error, we get:
Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'word word -word' +'word +word word'
Yep. We should wrap at the rightmost space in a line.
function wrap($text, $lineLength) { if (strlen($text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength), ' ') != 0) return substr ($text, 0, strrpos($text, ' ')) . "\n" . $this->wrap(substr($text, strrpos($text, ' ') + 1), $lineLength); return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); }
Simply replace the strpos()
with strrpos()
inside the second if
statement.
Step 9 - Other Failing Tests? Edge Cases?
Things are getting trickier. It's fairly hard to find a failing test ... or any test, for that matter, that was not yet written.
This is an indication that we are quite close to a final solution. But, hey, I just thought of a test that will fail!
function testItWraps2WordsOn3Lines() { $textToBeParsed = 'word word'; $maxLineLength = 3; $this->assertEquals("wor\nd\nwor\nd", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
But, I was wrong. It passes. Hmm... Are we done? Wait! What about this one?
function testItWraps2WordsAtBoundry() { $textToBeParsed = 'word word'; $maxLineLength = 4; $this->assertEquals("word\nword", $this->wrapper->wrap($textToBeParsed, $maxLineLength)); }
It fails! Excellent. When the line has the same length as the word, we want the second line to not begin with a space.
Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ 'word -word' + wor +d'
There are several solutions. We could introduce another if
statement to check for the starting space. That would fit in with the rest of the conditionals that we've created. But, isn't there a simpler solution? What if we just trim()
the text?
function wrap($text, $lineLength) { $text = trim($text); if (strlen($text) <= $lineLength) return $text; if (strpos(substr($text, 0, $lineLength), ' ') != 0) return substr ($text, 0, strrpos($text, ' ')) . "\n" . $this->wrap(substr($text, strrpos($text, ' ') + 1), $lineLength); return substr ($text, 0, $lineLength) . "\n" . $this->wrap(substr($text, $lineLength), $lineLength); }
There we go.
Step 10 - We Are Done
At this point, I can't invent any failing test to write. We must be done! We've now used TDD to build a simply, but useful, six-line algorithm.
A few words on stopping and "being done." If you use TDD, you force yourself to think about all sorts of situations. You then write tests for those situations, and, in the process, begin to understand the problem much better. Usually, this process results in an intimate knowledge of the algorithm. If you can't think of any other failing tests to write, does this mean that your algorithm is perfect? Not necessary, unless there is a predefined set of rules. TDD does not guarantee bug-less code; it merely helps you write better code that can be better understood and modified.
Even better, if you do discover a bug, it's that much easier to write a test that reproduces the bug. This way, you can ensure that the bug never occurs again - because you've tested for it!
Final Notes
You may argue that this process is not technically "TDD." And you're right! This example is closer to how many everyday programmers work. If you want a true "TDD as you mean it" example, please leave a comment below, and I'll plan to write one in the future.
Thanks for reading!
Comments