Login

Unit Testing

Having a formalized unit testing infrastructure for your projects will save you time in the long run, especially when looking for bugs in code. This article introduces you to setting up a unit testing framework. It is excerpted from chapter 6 of the book Advanced PHP Programming, written by George Schlossnagle (Sams; ISBN: 0672325616).

Testing and engineering are inextricably tied forever.

All code is tested at some point—perhaps during its implementation, during a dedicated testing phase, or when it goes live. Any developer who has launched broken code live knows that it is easier to test and debug code during development than after it goes into production.

Developers give many excuses for not testing code until it is too late. These are some of the popular ones:

The project is too rushed.

My code always works the first time.

The code works on my machine.

Let’s explore these excuses. First, projects are rushed because productivity lags. Productivity is directly proportional to the amount of debugging required to make code stable and working. Unfortunately, testing early and testing late are not equal cost operations. The problem is two-fold:

In a large code base that does not have a formalized testing infrastructure, it is hard to find the root cause of a bug. It’s a needle-in-a-haystack problem. Finding a bug in a 10-line program is easy. Finding a bug in 10,000 lines of included code is a tremendous effort.

As the code base grows, so do the number of dependencies between components. Seemingly innocuous changes to a “core” library—whether adding additional features or simply fixing a bug—may unintentionally break other portions of the application. This is known as refactoring. As the size and complexity of software grow, it becomes increasingly difficult to make these sorts of changes without incurring time costs and introducing new bugs.

All software has bugs. Any developer who claims that his or her software is always bug-free is living in a fantasy world.

System setups are all slightly different, often in ways that are hard to anticipate. Differing versions of PHP, differing versions of libraries, and different file system layouts are just a few of the factors that can cause code that runs perfectly on one machine to inexplicably fail on another.

Although there are no silver bullets to solve these problems, a good unit-testing infrastructure comes pretty close. A unit is a small section of code, such as a function or class method. Unit testing is a formalized approach to testing in which every component of an application (that is, every unit) has a set of tests associated with it. With an automated framework for running these tests, you have a way of testing an application constantly and consistently, which allows you to quickly identify functionality-breaking bugs and to evaluate the effects of refactoring on distant parts of the application. Unit testing does not replace full application testing; rather, it is a complement that helps you create more stable code in less time.

By creating persistent tests that you carry with the library for its entire life, you can easily refactor your code and guarantee that the external functionality has not inadvertently changed. Any time you make an internal change in the library, you rerun the test suite. If the tests run error-free, the refactoring has been successful. This makes debugging vague application problems easier. If a library passes all its tests (and if its test suite is complete), it is less suspicious as a potential cause for a bug.

Note - Unit testing tends to be associated with the Extreme Programming methodology. In fact, pervasive unit testing is one of the key tenets of Extreme Programming. Unit testing existed well before Extreme Programming, however, and can certainly be used independently of it. This book isn’t about singling out a particular methodology as the “one true style,” so it looks at unit testing as a standalone technique for designing and building solid code. If you have never read anything about Extreme Programming, you should check it out. It is an interesting set of techniques that many professional programmers live by. More information is available in the “Further Reading” section at the end of the chapter.

{mospagebreak title= An Introduction to Unit Testing}

To be successful, a unit testing framework needs to have certain properties, including the following:

Automated—The system should run all the tests necessary with no interaction from the programmer.

Easy to write—The system must be easy to use.

Extensible—To streamline efforts and minimize duplication of work, you should be able to reuse existing tests when creating new ones.

To actually benefit from unit testing, we need to make sure our tests have certain properties:

Comprehensive—Tests should completely test all function/class APIs. You should ensure not only that the function APIs work as expected, but also that they fail correctly when improper data is passed to them. Furthermore, you should write tests for any bugs discovered over the life of the library. Partial tests leave holes that can lead to errors when refactoring or to old bugs reappearing.

Reusable—Tests should be general enough to usefully test their targets again and again. The tests will be permanent fixtures that are maintained and used to verify the library over its entire life span.

Writing Unit Tests for Automated Unit Testing

For the testing framework discussed in this chapter, we will use PEAR’sPHPUnit. PHPUnit, like most of the free unit testing frameworks, is based closely on JUnit, Erich Gamma and Kent Beck’s excellent unit testing suite for Java.

Installing PHPUnit is just a matter of running the following (which most likely needs root access):

A unit test consists of a collection of test cases. A test case is designed to check the outcome of a particular scenario. The scenario can be something as simple as testing the result of a single function or testing the result of a set of complex operations.

A test case in PHPUnit is a subclass of the PHPUnit_Framework_TestCase class. An instance of PHPUnit_Framework_TestCase is one or several test cases, together with optional setup and tear-down code.

The simplest test case implements a single test. Let’s write a test to validate the behavior of a simple email address parser. The parser will break an RFC 822 email address into its component parts.

When you have a number of small test cases (for example, when checking that both the local part and the domain are split out correctly), you can avoid having to create a huge number of TestCase classes. To aid in this, a TestCase class can support multiple tests:

As a convenience, if you instantiate the PHPUnit_Framework_TestSuite object with the name of the TestCase class, $suite automatically causes any methods whose names begin with test to automatically register:

$suite = new
PHPUnit_Framework_TestSuite('EmailAddressTestCase');
// testLocalPart and testDomain are now
auto-registered
PHPUnit_TextUI_TestRunner::run($suite);

Note that if you add multiple tests to a suite by using addTest, the tests will be run in the order in which they were added. If you autoregister the tests, they will be registered in the order returned by get_class_methods() (which is how TestSuite extracts the test methods automatically).

Writing Inline and Out-of-Line Unit Tests

Unit tests are not only useful in initial development, but throughout the full life of a project. Any time you refactor code, you would like to be able to verify its correctness by running the full unit test suite against it. How do you best arrange unit tests so that they are easy to run, keep up-to-date, and carry along with the library?

There are two options for packaging unit tests. In the first case, you can incorporate your testing code directly into your libraries. This helps ensure that tests are kept up-to-date with the code they are testing, but it also has some drawbacks. The other option is to package your tests in separate files.

{mospagebreak title=Inline Packaging}

One possible solution for test packaging is to bundle your tests directly into your libraries. Because you are a tidy programmer, you keep all your functions in subordinate libraries. These libraries are never called directly (that is, you never create the page http://www.omniti.com/EmailAddress.inc). Thus, if you add your testing code so that it is run if and only if the library is called directly, you have a transparent way of bundling your test code directly into the code base.

What is happening here? The top of this block checks to see whether you are executing this file directly or as an include. $_SERVER['PHP_SELF'] is an automatic variable that gives the name of the script being executed. realpath($_SERVER[PHP_SELF]) returns the canonical absolute path for that file, and _ _FILE_ _ is a autodefined constant that returns the canonical name of the current file. If _ _FILE_ _ and realpath($_SERVER[PHP_SELF]) are equal, it means that this file was called directly; if they are different, then this file was called as an include. Below that is the standard unit testing code, and then the tests are defined, registered, and run.

Relative, Absolute, and Canonical Pathnames - People often refer to absolute and relative pathnames. A relative pathname is a one that is relative to the current directory, such as foo.php or ../scripts/foo.php. In both of these examples, you need to know the current directory to be able to find the files.

An absolute path is one that is relative to the root directory. For example, /home/george/scripts/ foo.php is an absolute path, as is /home/george//src/../scripts/./foo.php. (Both, in fact, point to the same file.)

A canonical path is one that is free of any /../, /./, or //. The function realpath() takes a relative or absolute filename and turns it into a canonical absolute path. /home/george/scripts/foo.php is an example of a canonical absolute path.

To test the EmailAddress class, you simply execute the include directly:

The test code might need to be manually separated out of commercial code before it ships.

There is no need to change the library to alter testing or vice versa. This keeps revision control on the tests and the code clearly separate.

PHP is an interpreted language, so the tests still must be parsed when the script is run, and this can hinder performance. In contrast, in a compiled language such as C++, you can use preprocessor directives such as #ifdef to completely remove the testing code from a library unless it is compiled with a special flag.

Embedded tests do not work (easily) for Web pages or for C extensions.