Testing your code is annoying, but the impact of not doing so can be orders of magnitude more annoying! In this article, we'll use test-driven development to write and test our code more effectively.
What is Test-Driven Development?
Since the dawn of the computer era, programmers and bugs have battled for supremacy. It's an inevitable occurrence. Even the greatest programmers fall prey to these anomalies. No code is safe. That's why we do testing. Programmers, at least sane ones, test their code by running it on development machines to make sure it does what it's supposed to.
Insane programmer who doesn't test his programs.
Image courtesy of http://www.internetannoyanceday.com
Test-driven development is a programming technique that requires you to write actual code and automated test code simultaneously. This ensures that you test your code—and enables you to retest your code quickly and easily, since it's automated.
How does it work?
Test-driven development, or TDD as we'll call it from now on, revolves around a short iterative development cycle that goes something like this:
- Before writing any code, you must first write an automated test for your code. While writing the automated tests, you must take into account all possible inputs, errors, and outputs. This way, your mind is not clouded by any code that's already been written.
- The first time you run your automated test, the test should fail—indicating that the code is not yet ready.
- Afterward, you can begin programming. Since there's already an automated test, as long as the code fails it, it means that it's still not ready. The code can be fixed until it passes all assertions.
- Once the code passes the test, you can then begin cleaning it up, via refactoring. As long as the code still passes the test, it means that it still works. You no longer have to worry about changes that introduce new bugs.
- Start the whole thing over again with some other method or program.
The test-driven development cycle
Image courtesy of http://en.wikipedia.org/wiki/Test-driven_development
Great, but how is this better than regular testing?
Have you ever purposefully skipped testing a program because:
- You felt it was a waste of time to test, since it was only a slight code change?
- You felt lazy testing everything again?
- You didn't have enough time to test because the project manager wanted it moved up to production ASAP?
- You told yourself you'd do it "tomorrow"?
- You had to choose between manual testing, or watching the latest episode of your favorite TV show (Big Bang Theory)?
Most of the time, nothing happens, and you successfully move your code to production without any problems. But sometimes, after you've moved to production, everything goes wrong. You're stuck fixing a hundred holes in a sinking ship, with more appearing every minute. You do not want to find yourself in this situation.
TDD was meant to eliminate our excuses. When a program has been developed using TDD, it allows us to make changes and test quickly and efficiently. All we need to do is run the automated tests, and voila! If it passes all automated tests, then we're good to go—if not, then it just means we broke something with the changes. By knowing which exact parts of the test failed, it also allows us to easily pinpoint at which part of the changes it broke, so it makes fixing the bugs easier.
I'm Sold. How Do We Do This?
There's a multitude of PHP automated testing frameworks out there we can use. One of the most widely-used testing frameworks is PHPUnit.
PHPUnit is a great testing framework, which can easily be integrated into your own projects, or other projects built on top of popular PHP frameworks.
For our purposes though, we won't need the multitude of functions that PHPUnit offers. Instead, we'll opt to create our tests using a much easier testing framework, called SimpleTest.
In the next steps, let's assume that we're developing a guestbook application where any user can add and view guestbook entries. Let's assume that the markup has been completed, and that we're simply making a class which contains the application logic of the guestbook, which is where the application inserts and reads to the database. The reading portion of this class is what we're going to develop and test.
Step 1. Set up SimpleTest
This is arguably the easiest step of all. Even this guy could do it:
Download SimpleTest here, and extract to a folder of your choice -- preferably the folder where you're going to develop your code, or your PHP include_path for easy access.
For this tutorial, I've set up the folder like so:
Index.php will run guestbook.php, and invoke the view method and display the entries. Inside the classes folder is where we'll put the guestbook.php class, and the test folder is where we place the simpletest library.
Step 2. Plan Your Attack
The second step, which is actually the most important one, is to start creating your tests. For this, you really need to plan and think about what your function will do, what possible inputs it will get, and the corresponding outputs it will send. This step resembles playing a game of chess—you need to know everything about your opponent (the program), including all his weaknesses (possible errors) and strengths (what happens if it successfully runs).
So for our guestbook application, let's lay down the schematics:
View
- This function will not have any inputs since it will just retrieve all of the entries from the database and send back the data to be printed out.
- It will return an array of guestbook records, stating the name of the poster and his message. If there are no records, then it should still return an empty array.
- If there are records, the array will have 1 or more values in it.
- At the same time, the array will have a specific structure, something like:
Array ( [0] => Array ( ['name'] = "Bob" ['message'] = "Hi, I'm Bob." ) [1] => Array ( ['name'] = "Tom" ['message'] = "Hi, I'm Tom." ) )
Step 3. Write a Test!
Now, we can write our first test. Let's start by creating a file called guestbook_test.php inside the test folder.
<?php require_once(dirname(__FILE__) . '/simpletest/autorun.php'); require_once('../classes/guestbook.php'); class TestGuestbook extends UnitTestCase { }
Then, let's convert what we've determined from step two,.
<?php require_once(dirname(__FILE__) . '/simpletest/autorun.php'); require_once('../classes/guestbook.php'); class TestGuestbook extends UnitTestCase { function testViewGuestbookWithEntries() { $guestbook = new Guestbook(); // Add new records first $guestbook->add("Bob", "Hi, I'm Bob."); $guestbook->add("Tom", "Hi, I'm Tom."); $entries = $guestbook->viewAll(); $count_is_greater_than_zero = (count($entries) > 0); $this->assertTrue($count_is_greater_than_zero); $this->assertIsA($entries, 'array'); foreach($entries as $entry) { $this->assertIsA($entry, 'array'); $this->assertTrue(isset($entry['name'])); $this->assertTrue(isset($entry['message'])); } } function testViewGuestbookWithNoEntries() { $guestbook = new Guestbook(); $guestbook->deleteAll(); // Delete all the entries first so we know it's an empty table $entries = $guestbook->viewAll(); $this->assertEqual($entries, array()); } }
Assertions make sure that a certain thing is what it's supposed to be—basically, it ensures that what's returned is what you're expecting it to return. For example, if a function is supposed to return true if it's successful, then in our test, we should assert that the return value is equal to true.
As you can see here, we test the viewing of the guestbook with entries and without. We check if these two scenarios pass our criteria from step two. You probably also noticed that each of our test functions start with the word 'test.' We did this because, when SimpleTest runs this class, it will look for all the functions that start with the word 'test' and run it.
In our test class, we've also used some assertion methods, such as assertTrue, assertIsA, and assertEquals. The assertTrue function checks whether or not a value is true. AssertIsA checks if a variable is of a certain type or class. And lastly, assertEquals checks if a variable is totally equal to a certain value.
There are other assertion methods provided by SimpleTest, which are:
assertTrue($x) |
Fail if $x is false |
assertFalse($x) |
Fail if $x is true |
assertNull($x) |
Fail if $x is set |
assertNotNull($x) |
Fail if $x not set |
assertIsA($x, $t) |
Fail if $x is not the class or type $t |
assertNotA($x, $t) |
Fail if $x is of the class or type $t |
assertEqual($x, $y) |
Fail if $x == $y is false |
assertNotEqual($x, $y) |
Fail if $x == $y is true |
assertWithinMargin($x, $y, $m) |
Fail if abs($x - $y) < $m is false |
assertOutsideMargin($x, $y, $m) |
Fail if abs($x - $y) < $m is true |
assertIdentical($x, $y) |
Fail if $x == $y is false or a type mismatch |
assertNotIdentical($x, $y) |
Fail if $x == $y is true and types match |
assertReference($x, $y) |
Fail unless $x and $y are the same variable |
assertClone($x, $y) |
Fail unless $x and $y are identical copies |
assertPattern($p, $x) |
Fail unless the regex $p matches $x |
assertNoPattern($p, $x) |
Fail if the regex $p matches $x |
expectError($x) |
Swallows any upcoming matching error |
assert($e) |
Fail on failed expectation object $e |
Assertion method list courtesy of http://www.simpletest.org/en/unit_test_documentation.html
Step 4. Fail to Win
Once you're finished writing the code, you should run the test. The first time you run the test, it SHOULD FAIL. If it doesn't, then it means that your test doesn't really test anything.
To run your test, simply run guestbook_test.php in your browser. You should see this first:
This happened because we haven't created our guestbook class yet. To do so, create guestbook.php inside your classes folder. The class should contain the methods we're planning to use, but shouldn't contain anything yet at first. Remember, we're writing the tests first before writing any code.
<?php class Guestbook { public function viewAll() { } public function add( $name, $message ) { } public function deleteAll() { } }
When you run the test again, it should look something like this:
As we can see here, our test is now winning by failing. This means that our test is now ready to get "answered."
Step 5. Answer Your Test by Writing Code
At some point, we've all felt like this when we're programming.
Image courtesy of http://fermentation.typepad.com/fermentation
Now that we have a working automated test, we can start writing code. Open up your guestbook.php class and start creating the answer to your test.
<?php class Guestbook { // To save time, instead of creating and connecting to a database, we're going to // simulate a "database" by creating a static entries array here. // It will be like we have two entries in the table. private static $_entries = array( array ( 'name' => 'Kirk', 'message' => 'Hi, I\'m Kirk.' ), array ( 'name' => 'Ted', 'message' => 'Hi, I\'m Ted.' ) ); public function viewAll() { // Here, we should retrieve all the records from the database. // This is simulated by returning the $_entries array return self::$_entries; } public function add( $name, $message ) { // Here, we simulate insertion into the database by adding a new record into the $_entries array // This is the correct way to do it: self::$_entries[] = array('name' => $name, 'message' => $message ); self::$_entries[] = array('notname' => $name, 'notmessage' => $message ); //oops, there's a bug here somewhere return true; } public function deleteAll() { // We just set the $_entries array to simulate self::$_entries = array(); return true; } }
This guestbook.php class has some bugs in it on purpose, so we can see what it looks like if our test fails.
Once we run our test, we should see something like this:
The test output shows us in which test and in which assertion our code failed. From this, we can easily pinpoint that line 16 and 17 was the assertion that threw the error.
<?php require_once(dirname(__FILE__) . '/simpletest/autorun.php'); require_once('../classes/guestbook.php'); class TestGuestbook extends UnitTestCase { ... ... ... $this->assertTrue(isset($entry['name'])); $this->assertTrue(isset($entry['message'])); ... ... ... }
This clearly tells us that the returned entry array did not have the correct array key. Based on this, we'll easily know which part of our code went wrong.
<?php class Guestbook { ... ... ... public function add( $name, $message ) { // Here, we simulate insertion into the database by adding a new record into the $_entries array self::$_entries[] = array('name' => $name, 'message' => $message ); //fixed! return true; } ... ... ... }
Now, when we run our test again, it should show us:
Step 6. Refactor and Refine Your Code
Since the code we're testing here is pretty simple, our testing and bug fixing didn't last very long. But if this was a more complex application, you'd have to make multiple changes to your code, make it cleaner so it's easier to maintain, and a lot of other things. The problem with this, though, is that change usually introduces additional bugs. This is where our automated test comes in—once we make changes, we can simply run the test again. If it still passes, then it means we didn't break anything. If it fails, we know that we made a mistake. It also informs us where the problem is, and, hopefully, how we'll be able to fix it.
Step 7. Rinse and Repeat
Eventually, when your program requires new functionality, you'll need to write new tests. That's easy! Rinse and repeat the procedures from step two (since your SimpleTest files should already be set up), and start the cycle all over again.
Conclusion
There are a lot more in-depth test-driven development articles out there, and even more functionality to SimpleTest than what was displayed in this article—things like mock objects, stubs, which make it easier to create tests. If you'd like to read more, Wikipedia's test-driven development page should set you on the right path. If you're keen on using SimpleTest as your testing framework, browse the online documentation and be sure to review its other features.
Testing is an integral part of the development cycle, however, it's too often the first thing to be cut when deadlines are imminent. Hopefully, after reading this article, you'll appreciate how helpful it is to invest in test-driven development.
What are your thoughts on Test-Driven Development? Is it something you're interested in implementing, or do you think it's a waste of time? Let me know in the comments!
Comments