Приглашаем посетить
Культурология (cult-lib.ru)

Writing Inline and Out-of-Line Unit Tests

Previous
Table of Contents
Next

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.

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 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.

To the bottom of EmailAddress.inc you can add this block:

if(realpath($_SERVER['PHP_SELF']) == _ _FILE_ _) {
  require_once "PHPUnit/Framework/TestSuite.php";
  require_once "PHPUnit/TextUI/TestRunner.php";
  class EmailAddressTestCase extends PHPUnit_Framework_TestCase{
    public function _ _construct($name) {
      parent::_ _construct($name);
    }
    public function testLocalPart() {
      $email = new EmailAddress("george@omniti.com");
      // check that the local part of the address is equal to 'george'
      $this->assertTrue($email->localPart == 'george');
    }
    public function testDomain() {
      $email = new EmailAddress("george@omniti.com");
      $this->assertEquals($email->domain, 'omniti.com');
    }
   }
   $suite = new PHPUnit_Framework_TestSuite('EmailAddressTestCase');
   PHPUnit_TextUI_TestRunner::run($suite);
}

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:

 (george@maya)[chapter-6]> php EmailAddress.inc
PHPUnit 1.0.0-dev by Sebastian Bergmann.

..

Time: 0.003005027771

OK (2 tests)

This particular strategy of embedding testing code directly into the library might look familiar to Python programmers because the Python standard library uses this testing strategy extensively.

Inlining tests has a number of positive benefits:

  • The tests are always with you.

  • Organizational structure is rigidly defined.

It has some drawbacks, as well:

  • 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.

Separate Test Packaging

Given the drawbacks to inlining tests, I choose to avoid that strategy and write my tests in their own files. For exterior tests, there are a number of different philosophies. Some people prefer to go the route of creating a t or tests subdirectory in each library directory for depositing test code. (This method has been the standard method for regression testing in Perl and was recently adopted for testing the PHP source build tree.) Others opt to place tests directly alongside their source files. There are organizational benefits to both of these methods, so it is largely a personal choice. To keep our examples clean here, I use the latter approach. For every library.inc file, you need to create a library.phpt file that contains all the PHPUnit_Framework_TestCase objects you define for it.

In your test script you can use a trick similar to one that you used earlier in this chapter: You can wrap a PHPUnit_Framework_TestSuite creation and run a check to see whether the test code is being executed directly. That way, you can easily run the particular tests in that file (by executing directly) or include them in a larger testing harness.

EmailAddress.phpt looks like this:

<?php
require_once "EmailAddress.inc";
require_once 'PHPUnit/Framework/TestSuite.php';
require_once 'PHPUnit/TextUI/TestRunner.php';

class EmailAddressTestCase extends PHPUnit_Framework_TestCase {
  public function _ _construct($name) {
    parent::_ _construct($name);
  }
  public function testLocalPart() {
    $email = new EmailAddress("george@omniti.com");
    // check that the local part of the address is equal to 'george'
    $this->assertTrue($email->localPart == 'george');
   }
   public function testDomain() {
     $email = new EmailAddress("george@omniti.com");
     $this->assertTrue($email->domain == 'omniti.com');
   }
}
if(realpath($_SERVER[PHP_SELF]) == _ _FILE_ _) {
  $suite = new PHPUnit_Framework_TestSuite('EmailAddressTestCase');
  PHPUnit_TextUI_TestRunner::run($suite);
}
?>

In addition to being able to include tests as part of a larger harness, you can execute EmailAddress.phpt directly, to run just its own tests:

PHPUnit 1.0.0-dev by Sebastian Bergmann.

..

Time: 0.0028760433197

OK (2 tests)

Running Multiple Tests Simultaneously

As the size of an application grows, refactoring can easily become a nightmare. I have seen million-line code bases where bugs went unaddressed simply because the code was tied to too many critical components to risk breaking. The real problem was not that the code was too pervasively used; rather, it was that there was no reliable way to test the components of the application to determine the impact of any refactoring.

I'm a lazy guy. I think most developers are also lazy, and this is not necessarily a vice. As easy as it is to write a single regression test, if there is no easy way to test my entire application, I test only the part that is easy. Fortunately, it's easy to bundle a number of distinct TestCase objects into a larger regression test. To run multiple TestCase objects in a single suite, you simply use the addTestSuite() method to add the class to the suite. Here's how you do it:

<?php
require_once "EmailAddress.phpt";
require_once "Text/Word.phpt";
require_once "PHPUnit/Framework/TestSuite.php";
require_once "PHPUnit/TextUI/TestRunner.php";

$suite = new PHPUnit_Framework_TestSuite();
$suite->addTestSuite('EmailAddressTestCase');
$suite->addTestSuite('Text/WordTestCase');

PHPUnit_TextUI_TestRunner::run($suite);
?>

Alternatively, you can take a cue from the autoregistration ability of PHPUnit_Framework_TestSuite to make a fully autoregistering testing harness. Similarly to the naming convention for test methods to be autoloaded, you can require that all autoloadable PHPUnit_Framework_TestCase subclasses have names that end in TestCase. You can then look through the list of declared classes and add all matching classes to the master suite. Here's how this works:

<?php
require_once "PHPUnit/FrameWork/TestSuite.php";

class TestHarness extends PHPUnit_Framework_TestSuite {
  private $seen = array();
  public function _ _construct() {
    $this = parent::_ _construct();
    foreach(get_declared_classes() as $class) {
      $this->seen[$class] = 1;
    }
  }
  public function register($file) {
    require_once($file);
    foreach(get_declared_classes() as $class) {
      if(array_key_exists($class, $this->seen)) {
        continue;
      }
      $this->seen[$class] = 1;
      //  ZE lower-cases class names, so we look for "testcase"
      if(substr($class, -8, 8) == 'testcase') {
        print "adding $class\n";
        $this->addTestSuite($class);
      }
    }
  }
}
?>

To use the TestHarness class, you simply need to register the files that contain the test classes, and if their names end in TestCase, they will be registered and run. In the following example, you write a wrapper that uses TestHarness to autoload all the test cases in EmailAddress.phpt and Text/Word.phpt:

<?php
require_once "TestHarness.php";
require_once "PHPUnit/TextUI/TestRunner.php";

$suite = new TestHarness();
$suite->register("EmailAddress.phpt");
$suite->register("Text/Word.phpt");
PHPUnit_TextUI_TestRunner::run($suite);
?>

This makes it easy to automatically run all the PHPUnit_Framework_TestCase objects for a project from one central location. This is a blessing when you're refactoring central libraries in an API that could affect a number of disparate parts of the application.


Previous
Table of Contents
Next