Beginning Unit Tests

The most common thing I hear from people who don’t unit test but would like to is that they don’t know where to start. I’ve documented here the basics of how to write an initial unit test, given some knowledge of Zend Framework and phpunit. I’ve also tried to cover the main points of conflict that developers who have not previously automated their tests tend to run into.

Testing goes from “want” to “have”. So start with a “want”, e.g. “title must be html escaped”. Presumably this “want” will be a description of a new feature or solution to a bug in your software.

Document your want in a test case:

// A PHPUnit subclass with some fixtures to initialise your application
// and make it runnable.
class ProductPageTestCase 
  extends \Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase 
{

  public function setUp()
  {
     // ...
  }

  public function testTitleMustBeHtmlEscaped() 
  {
  }
}

Work out how to express your “want” in terms of an assertion. Start at the assertion and work back:

$this->assertContains("<title>" . htmlspecialchars("<") . "</title>", $content);

(I’m using htmlspecialchars because wordpress really loves to ruin escaping in code samples, normally you would write the escaped less-than in the most simple way).

Next work out how to run the page. In the case of a Zend controller test you run:

$this->dispatch("/product-page/1");
$content = $this->getResponse()->getContent();

All that’s happening here is that Zend is simulating the dispatch process that would happen when it receives a real HTTP request.

Work out how you can make the content contain your use case:

$this->database->insert('product', array('product_name' => "<"));

This is the "fixture, run, test" trifecta which you will see in all tests.

Note that what I've written in the example is really an "integration" or "component" test which is testing the full application stack rather than one completely isolated unit. This is usually what you must start with if your application does not currently have any test automation and will give you the most testing value in the fastest way. A unit test of your application classes would need to avoid dependencies as much as possible in order to avoid failing every time a dependency changes. The method of writing the tests is still essentially the same.

The fixture is a point where many get confused. Surely you have to make massive changes to your app to make your database editable from within a test, right? The answer is yes — and that's expected. You are forced into a design by your tests which requires that you make dependencies injectable and mockable. This requires that your application is modular and decoupled which is generally a good thing so the tests are error-checking your design too.

Tools like Zend's service locator are intended to make this kind of design easy, for example a service factory can return an in-memory sqlite for your database and mock transports for your apis when in a test environment. If you're using a modern framework like Zend you may find that even if you're not automating your testing now you are already using the tools you need to do it (but you will need to change the specifics of your implementation to take advantage of that).

In Zend, a test setup fixture might look something like this:

public function setUp() 
{
   // I prefer to use a config which uses my autoloads but skips production
   // credentials and overrides everything to mocked values.  This gives a 
   // short reusable test setup but means your service factories have to do 
   // more work.
   $this->setApplicationConfig(require APP_ROOT . "/config/environments/unit-tests.php");
   $services = $this->getApplicationServiceLocator();
   // You can alter the services manually too:
   $db = new \PDO("sqlite::memory:");
   $db->exec("CREATE TABLE product ( ... )");
   $services->setService("MyAppDb", $db);
}

There are several choices for implementing fixtures. My example simply manipulates the persistence layer directly, which is usually the only choice for existing untested software (at least to begin with). Fixtures can be more readable and further encourage good design if you involve your application's real models (or mocks thereof) rather than bypassing them and using the persistence layer directly.

Generally when you're modifying an existing test case, it should be very obvious how to create a new test method since new methods should look a lot like the existing ones. Structural duplication is expected in a unit test since simplicity — especially avoiding deeply layered software — is more valuable than duplication in test maintenance.

It is also important to be aware that the test will make it hard for you to change the behaviour of that use case or of its fixture. This will make it necessary for you to maintain backwards compatibility features for the sake of your tests. This can be awkward and seems to be a nasty surprise for new developers who are used to breaking compatibility at will. Typically large applications worked on by multiple people need to maintain backwards compatibility anyway so this is another case of the unit tests forcing you into a particular design philosophy which is usually a good idea anyway. Nevertheless this seems to be a huge annoyance among new unit testers so it's worth keeping in mind how your software design needs to change.

Once your test is complete, watch it fail. It should fail in the right way; so in the example above you should expect to see an invalid HTML title produced. This is important because it verifies that your tests are detecting the condition that you intended.

Next implement the feature. Do this in the fastest possible way you can. Don't worry about what sins you have to commit to make the thing happen. Repeatedly run the test until it passes. This is a great way to break out of design worry deadlock and over-engineering — it does not matter how it works so long as it works.

If you find yourself writing lots of print statements at the implementation stage then your test may be too broad. Try abstracting and writing unit tests of the specific part that you are working with. Once again the unit testing is giving you a signal to change the design of your software. In general your tests should be replacing debugging, not adding additional work. Avoiding the debugging process is probably the most important factor for making TDD a fast development process. This can be a hard habit to break and needs some practice. Try implementing a new assertion every time you would have added a print statement.

Once the test passes you can work on cleaning up the code. Remove the sins you committed, clear up and make things pretty. You should not generally need to make major architectural changes at this point; your main job is to remove anything that could be interpreted as duplication, as well as getting rid of future risks like global variables.

If you do this right, you can develop features very quickly without putting any major hurdles into the product for the next time that you have to modify it. Quality trends upwards and your existing code has less impact on the cost of new features. The more you keep the product supple, the more your simple changes become trivial, and your complex changes become achievable.

There is more to testing than this, of course, but the general mantras are:

  • always work from "want" to "have"
  • keep your tests as simple as possible
  • work in small increments
  • don't duplicate code
  • remember the cleanup stage
  • allow the unit tests to steer your design decisions
Beginning Unit Tests