Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Advanced PHP Programming

.pdf
Скачиваний:
67
Добавлен:
14.04.2015
Размер:
7.82 Mб
Скачать

158 Chapter 6 Unit Testing

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

Writing Inline and Out-of-Line Unit Tests

159

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:

nThe tests are always with you.

nOrganizational structure is rigidly defined.

It has some drawbacks, as well:

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

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

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

nEmbedded 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

160Chapter 6 Unit Testing

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)

Writing Inline and Out-of-Line Unit Tests

161

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

162 Chapter 6 Unit Testing

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.

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:

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

Additional Features in PHPUnit

163

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

164 Chapter 6 Unit Testing

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=’’)

TEAM

FLY

Additional Features in PHPUnit

165

 

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

}

}

166 Chapter 6 Unit Testing

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)

{

Additional Features in PHPUnit

167

$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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]