Writing Inline and Out-of-Line Unit TestsUnit 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 PackagingOne 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. 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:
It has some drawbacks, as well:
Separate Test PackagingGiven 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 SimultaneouslyAs 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. |
Главная
|