TDD-Deciphered.com

Part 4: First test

02/03/2010
As promised, it's time to start coding. I've set up a repository with the directory structure mentioned in the previous post at: http://services.phase.org/svn/tdd-enigma/trunk . This is world-readable (but not writable) so you'll be able to check out the code at any revision and follow what's being done. If you want to use the code, consider it to be under GPL v2.

So, we'll start with a checkout of that code:
Silverbox:Enigma wechsler$ svn co http://services.phase.org/svn/tdd-enigma/trunk/ tdd-enigma
A    tdd-enigma/tests
A    tdd-enigma/tests/Enigma
A    tdd-enigma/library
A    tdd-enigma/library/Enigma
A    tdd-enigma/cli
A    tdd-enigma/docs
Checked out revision 1.
and dive right into the tests. As noted before, in pure TDD, we test first and ask questions code later.

What can we test from what we've decided so far? Well, we decided above that a Rotor was going to implement the Enigma_Encryptor_Interface, so we'll create a test to check that.

We add the following code in tests/Enigma/RotorTest.php:
  1. <?php
  2. class Enigma_RotorTest extends PHPUnit_Framework_TestCase
  3. {
  4.  
  5. public function testRotorIsAnEncrytor ()
  6. {
  7. $rotor = new Enigma_Rotor();
  8. $this->assertTrue($rotor instanceof Enigma_Encryptor_Interface);
  9. }
  10. }
So, what on earth's going on here? Well, we've written a PHPUnit test case that will confirm now, and at any point in the future, that creating a rotor gives us a class that implements the Enigma_Encryptor_Interface. If this code works, we keep it, and in two years when we need to see if our simulator still works, we can run it to make sure.

If you¿ve never written a PHPUnit test case before, some elements and functions will be unfamiliar:

PHPUnit_Framework_TestCase: To create a test case, simply create a class that extends this class - all the rest of the work will be done for you.

public function test...: Any functions starting with the word "test" in a test case are automatically run when you trigger PHPUnit.

$this->assertTrue(): Extending PHPUnit_Framework_TestCase gives your class a number of assert* functions, which you use to confirm that your code is doing what's expected.

And that's pretty much it for the base elements of PHP unit testing. Everything else is just a variation on the above.

Running the test case is simple, presuming PHPUnit's properly installed on your system: just go to the command line and, in the appropriate directory, run:
phpunit RotorTest.php
Do not run php RotorTest.php Only twits who haven't had enough coffee do that, and then wonder why the output's blank. I do it about twice a week.

So, what happened when we ran that? Well, if everything went to plan, it looked something like:
Silverbox:Enigma wechsler$ phpunit RotorTest.php 
PHPUnit 3.4.5 by Sebastian Bergmann.

Fatal error: Class 'Enigma_Rotor' not found in 
   /Library/WebServer/phaseTwo/enigmaProject/tests/Enigma/RotorTest.php on line 7
Wait a sec - that's a Fatal Error. How is that everything going to plan? Well, as we haven't written the class Enigma_Rotor, that's the Expected Behaviour. If it worked, we'd have reason to be scared.

"Fail First" is generally a good rule to follow in TDD. It means you know that your test actually does something, and that you've (probably) fixed the right thing when it works. If you don't fail first, it's very easy to write tests that tell you nothing.

So, failure is success! Hooray!

Fine, but you're unlikely to get paid for it. Let's go for a form of success that's also success. And, following the rules of TDD, we'll do it in the simplest way possible.

First, we need an interface Enigma_Encryptor_Interface, and a class Enigma_Rotor that implements it.

Inside our library/Enigma directory, we add:

Rotor.php:
  1. <?php
  2. class Enigma_Rotor implements Enigma_Encryptor_Interface {}
Encryptor/Interface.php:
  1. <?php
  2. interface Enigma_Encryptor_Interface {}
And that'll do. No functions yet, for now we're deliberately stretching the KISS (Keep It Simple, Stupid) principle to breaking point. Right now, we just want our tests to run and pass. Detail comes later.

Back in the tests/Enigma directory, we can re-run phpunit RotorTest.php. And we'll get exactly the same result as last time, because our test code has no idea whatsoever how to find the class and interface files.

A quick hack solves that. At the top of tests/Enigma/RotorTest.php, add:
  1. require_once('../../library/Enigma/Encryptor/Interface.php');
  2. require_once('../../library/Enigma/Rotor.php');
and run phpunit RotorTest.php again:
Silverbox:Enigma wechsler$ phpunit RotorTest.php 
PHPUnit 3.4.5 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 5.75Mb

OK (1 test, 1 assertion)
Wow! Success! Our first ever unit test passed!

Except- right now, you're probably still cringing from those require_once lines in the test file. If you're not cringing, you really should be. Placing all the require()s for all the code you want to run at the top of the main script file is, frankly, utterly daft.

But, just for the sake of getting that test running, we'll allow ourselves that hack - for about five seconds!

The pattern of Test-Driven Development is often described as "Red-Green-Refactor". As we're working on the command line, it's a bit less colourful, but the idea's the same:

Red: Write a failing test. Run the test, see it fail.
Green: Write the code that makes the test pass. Run the test, see it pass.
Refactor: Tidy up any shortcuts you took to get to green. Run the test, make sure it still passes.

We've done Red and Green, but we used a nasty hack to get there. Let's refactor away that hack. The best way to get there in this case is to add an autoloader to our code.

We'll put the loader in a new class, Tools_Autoloader (I want to keep this separate from the main Enigma simulator code for now). In a new file, library/Tools/Autoloader.php, add the following very simple canned autoloader:
  1. <?php
  2. class Tools_Autoloader {
  3.  
  4. public function loadClass($class)
  5. {
  6. if(!defined('LIBRARY_ROOT'))
  7. {
  8. throw new Exception('LIBRARY_ROOT must be defined');
  9. }
  10.  
  11. $classPath=preg_replace('/_/',DIRECTORY_SEPARATOR,$class);
  12. $classFile=LIBRARY_ROOT.DIRECTORY_SEPARATOR.$classPath.'.php';
  13. if(file_exists($classFile) && is_readable($classFile)) {
  14. require($classFile);
  15. }
  16. }
  17. }
And in library/initialise.php, add:
  1. <?php
  2. define('LIBRARY_ROOT',dirname(__FILE__));
  3. require_once(LIBRARY_ROOT. DIRECTORY_SEPARATOR . 'Tools' .
  4. DIRECTORY_SEPARATOR . 'Autoloader.php');
  5. spl_autoload_register(array(new Tools_Autoloader,'loadClass'));
And finally, we'll use this in our test script instead of those requires. In tests/Enigma/RotorTest.php, replace the two require()s with:
  1. require_once('../../library/initialise.php');
and re-run the tests. They should pass, and we've finished our first complete Red-Green-Refactor cycle and so we're ready to check the code in. Our autoloader's not perfect, but it'll certainly do us for now.

We added quite a lot of code in this refactoring cycle, without adding any new tests for them. It may well be worth adding some tests for that autoloader - but again, for the sake of keeping moving, I'll leave it for now.

Having added all the new files, we can commit version 2:
wechsler$ svn commit -m 'First red-green cycle & autoloader'
Sending        .
Adding         library/Enigma/Encryptor
Adding         library/Enigma/Encryptor/Interface.php
Adding         library/Enigma/Rotor.php
Adding         library/Tools
Adding         library/Tools/Autoloader.php
Adding         library/initialise.php
Adding         tests/Enigma/RotorTest.php
Transmitting file data .....
Committed revision 2.
In part 5, we'll take a closer look at this ethos of modular testing, and explain why it's not quite as artificial as our first example might make it look!

All content copyright Richard George (richard@phase.org), 2009-2010

Sponsored links to recommended books: