Приглашаем посетить
Романтизм (19v-euro-lit.niv.ru)

Additional Features in PHPUnit

Previous
Table of Contents
Next

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 sugaror ease-of-use featuresin 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.

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 example 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");
  }
}

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);
}

Using Graphical Interfaces

Because PHP is a Web-oriented language, you might want an HTML-based user interface for running your unit tests. PHPUnit comes bundled with this ability, using PHPUnit_WebUI_TestRunner::run(). This is in fact a nearly identical framework to TextUI; it simply uses its own listener to handle generate HTML-beautified output.

Hopefully, in the future some of the PHP Integrated Development Environments (IDEs; programming GUIs) will expand their feature sets to include integrated support for unit testing (as do many of the Java IDEs). Also, as with PHP-GTK (a PHP interface to the GTK graphics library API that allows for Windows and X11 GUI development in PHP), we can always hope for a PHP-GTK front end for PHPUnit. In fact, there is a stub for PHPUnit_GtkUI_TestRunner in the PEAR repository, but at this time it is incomplete.


Previous
Table of Contents
Next