Unit Testing in Detail

Last week, we introduced you to setting up a unit testing framework. This week, you will learn about running multiple tests simultaneously, creating more informative error messages, and more. This article, the second of three parts, is excerpted from chapter 6 of the book Advanced PHP Programming, written by George Schlossnagle (Sams; ISBN: 0672325616).

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 $classn"; $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.

{mospagebreak title=Additional Features in PHPUnit}

One of the benefits of using an even moderately mature piece of open-source software is that it usually has a good bit of sugar—or ease-of-use features—in it. As more developers use it, convenience functions are added to suit developers’ individual styles, and this often produces a rich array of syntaxes and features.


Feature Creep - The addition of features over time in both open-source and commercial software is often a curse as much as it is a blessing. As the feature set of an application grows, two unfortunate things often happen:

  • Some features become less well maintained than others. How do you then know which features are the best to use?

  • Unnecessary features bloat the code and hinder maintainability and performance.

Both of these problems and some strategies for combating them are discussed in Chapter 8, “Designing a Good API.”


Creating More Informative Error Messages

Sometimes you would like a more informative message than this:

PHPUnit 1.0.0-dev by Sebastian Bergmann.

.F.

Time: 0.00583696365356
There was 1 failure:
1) TestCase emailaddresstestcase->testlocalpart()
failed: expected true, actual false FAILURES!!! Tests run: 2, Failures: 1, Errors: 0.

Especially when a test is repeated multiple times for different data, a more informative error message is essential to understanding where the break occurred and what it means. To make creating more informative error messages easy, all the assert functions that TestCase inherit from PHPUnit::Assert support free-form error messages. Instead of using this code:

function testLocalPart() {
 $email = new EmailAddress("georg@omniti.com");
 // check that the local part of the address is
equal to 'george' $this->assertTrue($email->localPart == 'george'); }

which generates the aforementioned particularly cryptic message, you can use a custom message:

function testLocalPart() {
 $email = new EmailAddress("georg@omniti.com");
 // check that the local part of the address is
equal to 'george' $this->assertTrue($email->localPart == 'george', "localParts: $email->localPart of $email->address
!= 'george'"); }

This produces the following much clearer error message:

PHPUnit 1.0.0-dev by Sebastian Bergmann.

.F.

Time: 0.00466096401215
There was 1 failure:
1) TestCase emailaddresstestcase->testlocalpart()
failed: local name: george of george@omniti.com != georg FAILURES!!! Tests run: 2, Failures: 1, Errors: 0.

Hopefully, by making the error message clearer, we can fix the typo in the test.

{mospagebreak title=Adding More Test Conditions}

With a bit of effort, you can evaluate the success or failure of any test by using assertTrue. Having to manipulate all your tests to evaluate as a truth statement is painful, so this section provides a nice selection of alternative assertions.

The following example tests whether $actual is equal to $expected by using ==:

assertEquals($expected, $actual, $message='')

If $actual is not equal to $expected, a failure is generated, with an optional message.

The following example:

$this->assertTrue($email->localPart === 'george');

is identical to this example:

$this->assertEquals($email->localPart, 'george');

The following example fails, with an optional message if $object is null:

assertNotNull($object, $message = '')

The following example fails, with an optional message if $object is not null:

assertNull($object, $message = '')

The following example tests whether $actual is equal to $expected, by using ===:

assertSame($expected, $actual, $message='')

If $actual is not equal to $expected, a failure is generated, with an optional message.

The following example tests whether $actual is equal to $expected, by using ===:

assertNotSame($expected, $actual, $message='')

If $actual is equal to $expected, a failure is generated, with an optional message.

The following example tests whether $condition is true:

assertFalse($condition, $message='')

If it is true, a failure is generated, with an optional message.

The following returns a failure, with an optional message, if $actual is not matched by the PCRE $expected:

assertRegExp($expected, $actual, $message='')

For example, here is an assertion that $ip is a dotted-decimal quad:

// returns true if $ip is 4 digits separated
by '.'s (like an ip address) $this->assertRegExp('/d+.d+.d+.d+/',$ip);

The following example generates a failure, with an optional message:

fail($message='')

The following examples generates a success:

pass()

Using the setUp() and tearDown() Methods

Many tests can be repetitive. For example, you might want to test EmailAddress with a number of different email addresses. As it stands, you are creating a new object in every test method. Ideally, you could consolidate this work and perform it only once. Fortunately, TestCase has the setUp and tearDown methods to handle just this case. setUp() is run immediately before the test methods in a TestCase are run, and tearDown() is run immediately afterward.

To convert EmailAddress.phpt to use setUp(), you need to centralize all your prep work:

class EmailAddressTestCase extends
PHPUnit_Framework_TestCase{ protected $email; protected $localPart; protected $domain; function _ _construct($name) { parent::_ _construct($name); } function setUp() { $this->email = new
EmailAddress("george@omniti.com"); $this->localPart = 'george'; $this->domain = 'omniti.com'; } function testLocalPart() { $this->assertEquals($this->email->localPart,
$this->localPart, "localParts: ".$this->email->localPart. " of ".$this->email->address." != $this->localPart"); } function testDomain() { $this->assertEquals($this->email->domain,
$this->domain, "domains: ".$this->email->domain. " of $this->email->address != $this->domain"); } }

{mospagebreak title=Adding Listeners}

When you execute PHPUnit_TextUI_TestRunner::run(), that function creates a PHPUnit_Framework_TestResult object in which the results of the tests will be stored, and it attaches to it a listener, which implements the interface PHPUnit_Framework_TestListener. This listener handles generating any output or performing any notifications based on the test results.

To help you make sense of this, here is a simplified version of PHPUnit_TextUI_TestRunner::run(), myTestRunner(). MyTestRunner() executes the tests identically to TextUI, but it lacks the timing support you may have noticed in the earlier output examples:

require_once "PHPUnit/TextUI/ResultPrinter.php";
require_once "PHPUnit/Framework/TestResult.php"; 

function myTestRunner($suite) 
{
 $result = new PHPUnit_Framework_TestResult;
 $textPrinter = new PHPUnit_TextUI_ResultPrinter;
 $result->addListener($textPrinter);
 $suite->run($result);
 $textPrinter->printResult($result);
}

PHPUnit_TextUI_ResultPrinter is a listener that handles generating all the output we’ve seen before. You can add additional listeners to your tests as well. This is useful if you want to bundle in additional reporting other than simply displaying text. In a large API, you might want to alert a developer by email if a component belonging to that developer starts failing its unit tests (because that developer might not be the one running the test). You can write a listener that provides this service:

<?php
require_once "PHPUnit/Framework/TestListener.php";

class EmailAddressListener implements
PHPUnit_Framework_TestListener { public $owner = "develepors@example.foo"; public $message = ''; public function addError(PHPUnit_Framework_Test
$test, Exception $e) { $this->message .= "Error in
".$test->getName()."n"; $this->message .= "Error message:
".$e->getMessage()."n"; } public function addFailure(PHPUnit_Framework_Test
$test, PHPUnit_Framework_AssertionFailedError
$e) { $this->message .= "Failure in
".$test->getName()."n"; $this->message .= "Error message:
".$e->getMessage()."n"; } public function startTest(PHPUnit_Framework_Test
$test) { $this->message .= "Beginning of test
".$test->getName()."n"; } public function endTest(PHPUnit_Framework_Test
$test) { if($this->message) { $owner =
isset($test->owner)?$test->owner:$this->owner; $date = strftime("%D %H:%M:%S"); mail($owner, "Test Failed at $date",
$this->message); } } } ?>

Remember that because EmailAddressListener implements PHPUnit_Framework_TestListener (and does not extend it), EmailAddressListener must implement all the methods defined in PHPUnit_Framework_TestListener, with the same prototypes.

This listener works by accumulating all the error messages that occur in a test. Then, when the test ends, endTest() is called and the message is dispatched. If the test in question has an owner attribute, that address is used; otherwise, it falls back to developers@example.foo.

To enable support for this listener in myTestRunner(), all you need to do is add it with addListener():

function myTestRunner($suite)
{
 $result = new PHPUnit_Framework_TestResult;
 $textPrinter = new PHPUnit_TextUI_ResultPrinter;
 $result->addListener($textPrinter);
 $result->addListener(new EmailAddressListener);
 $suite->run($result);
 $textPrinter->printResult($result);
}

Please check back next week for the conclusion of this article.

[gp-comments width="770" linklove="off" ]
antalya escort bayan antalya escort bayan