Copyright © 2005, 2006, 2007, 2008, 2009 Sebastian Bergmann
Edition for PHPUnit 3.1. Updated on 2009-12-29.
Even good programmers make mistakes. The difference between a good programmer and a bad programmer is that the good programmer uses tests to detect his mistakes as soon as possible. The sooner you test for a mistake the greater your chance of finding it and the less it will cost to find and fix. This explains why leaving testing until just before releasing software is so problematic. Most errors do not get caught at all, and the cost of fixing the ones you do catch is so high that you have to perform triage with the errors because you just cannot afford to fix them all.
Testing with PHPUnit is not a totally different activity from what you should already be doing. It is just a different way of doing it. The difference is between testing, that is, checking that your program behaves as expected, and performing a battery of tests, runnable code-fragments that automatically test the correctness of parts (units) of the software. These runnable code-fragments are called unit tests.
In this chapter we will go from simple print-based
testing code to a fully automated test. Imagine that we have been asked
to test PHP's built-in array. One bit of functionality
to test is the function sizeof(). For a newly created
array we expect the sizeof() function to return
0. After we add an element, sizeof()
should return 1.
Example 1.1 shows what
we want to test.
Example 1.1: Testing Array and sizeof()
<?php
$fixture = array();
// $fixture is expected to be empty.
$fixture[] = 'element';
// $fixture is expected to contain one element.
?>
A really simple way to check whether we are getting the results we
expect is to print the result of sizeof() before
and after adding the element (see
Example 1.2).
If we get 0 and then 1,
array and sizeof() behave as
expected.
Example 1.2: Using print to test Array and sizeof()
<?php
$fixture = array();
print sizeof($fixture) . "\n";
$fixture[] = 'element';
print sizeof($fixture) . "\n";
?>
0 1
Now, we would like to move from tests that require manual interpretation
to tests that can run automatically. In
Example 1.3, we write
the comparison of the expected and actual values into the test code and
print ok if the values are equal. If we ever see a
not ok message, we know something is wrong.
Example 1.3: Comparing expected and actual values to test Array and sizeof()
<?php
$fixture = array();
print sizeof($fixture) == 0 ? "ok\n" : "not ok\n";
$fixture[] = 'element';
print sizeof($fixture) == 1 ? "ok\n" : "not ok\n";
?>
ok ok
We now factor out the comparison of expected and actual values into a function that raises an Exception when there is a discrepancy (Example 1.4). This gives us two benefits: the writing of tests becomes easier and we only get output when something is wrong.
Example 1.4: Using an assertion function to test Array and sizeof()
<?php
$fixture = array();
assertTrue(sizeof($fixture) == 0);
$fixture[] = 'element';
assertTrue(sizeof($fixture) == 1);
function assertTrue($condition)
{
if (!$condition) {
throw new Exception('Assertion failed.');
}
}
?>
The test is now completely automated. Instead of just testing as we did with our first version, with this version we have an automated test.
The goal of using automated tests is to make fewer mistakes. While your code will still not be perfect, even with excellent tests, you will likely see a dramatic reduction in defects once you start automating tests. Automated tests give you justified confidence in your code. You can use this confidence to take more daring leaps in design (Refactoring), get along with your teammates better (Cross-Team Tests), improve relations with your customers, and go home every night with proof that the system is better now than it was this morning because of your efforts.
So far, we only have two tests for the array built-in
and the sizeof() function. When we start to test the
numerous array_*() functions PHP offers, we will need
to write a test for each of them. We could write the infrastructure for
all these tests from scratch. However, it is much better to write a
testing infrastructure once and then write only the unique parts of each
test. PHPUnit is such an infrastructure.
A framework such as PHPUnit has to resolve a set of constraints, some of which seem always to conflict with each other. Simultaneously, tests should be:
If it's hard to learn how to write tests, developers will not learn to write them.
If tests are not easy to write, developers will not write them.
Test code should contain no extraneous overhead so that the test itself does not get lost in noise that surrounds it.
The tests should run at the touch of a button and present their results in a clear and unambiguous format.
Tests should run fast so so they can be run hundreds or thousands of times a day.
The tests should not affect each other. If the order in which the tests are run changes, the results of the tests should not change.
We should be able to run any number or combination of tests together. This is a corollary of isolation.
There are two main clashes between these constraints:
Tests do not generally require all the flexibility of a programming language. Many testing tools provide their own scripting language that only includes the minimum necessary features for writing tests. The resulting tests are easy to read and write because they have no noise to distract you from the content of the tests. However, learning yet another programming language and set of programming tools is inconvenient and clutters the mind.
If you want the results of one test to have no effect on the results of another test, each test should create the full state of the world before it begins to execute and return the world to its original state when it finishes. However, setting up the world can take a long time: for example connecting to a database and initializing it to a known state using realistic data.
PHPUnit attempts to resolve these conflicts by using PHP as the testing language. Sometimes the full power of PHP is overkill for writing little straight-line tests, but by using PHP we leverage all the experience and tools programmers already have in place. Since we are trying to convince reluctant testers, lowering the barrier to writing those initial tests is particularly important.
PHPUnit errs on the side of isolation over quick execution. Isolated tests are valuable because they provide high-quality feedback. You do not get a report with a bunch of test failures, which were really caused because one test at the beginning of the suite failed and left the world messed up for the rest of the tests. This orientation towards isolated tests encourages designs with a large number of simple objects. Each object can be tested quickly in isolation. The result is better designs and faster tests.
PHPUnit assumes that most tests succeed and it is not worth reporting the details of successful tests. When a test fails, that fact is worth noting and reporting. The vast majority of tests should succeed and are not worth commenting on except to count the number of tests that run. This is an assumption that is really built into the reporting classes, and not into the core of PHPUnit. When the results of a test run are reported, you see how many tests were executed, but you only see details for those that failed.
Tests are expected to be fine-grained, testing one aspect of one object. Hence, the first time a test fails, execution of the test halts, and PHPUnit reports the failure. It is an art to test by running in many small tests. Fine-grained tests improve the overall design of the system.
When you test an object with PHPUnit, you do so only through the object's public interface. Testing based only on publicly visible behaviour encourages you to confront and solve difficult design problems earlier, before the results of poor design can infect large parts of the system.
PHPUnit should be installed using the PEAR Installer. This installer is the backbone of PEAR, which provides a distribution system for PHP packages, and is shipped with every release of PHP since version 4.3.0.
The PEAR channel (pear.phpunit.de) that
is used to distribute PHPUnit needs to be registered with the local PEAR
environment:
pear channel-discover pear.phpunit.deThis has to be done only once. Now the PEAR Installer can be used to install packages from the PHPUnit channel:
pear install phpunit/PHPUnit
After the installation you can find the PHPUnit source files inside your
local PEAR directory; the path is usually
/usr/lib/php/PHPUnit.
Although using the PEAR Installer is the only supported way to install PHPUnit, you can install PHPUnit manually. For manual installation, do the following:
Download a release archive from
http://pear.phpunit.de/get/
and extract it to a directory that is listed in the
include_path of your php.ini
configuration file.
Prepare the phpunit script:
Rename the pear-phpunit script to
phpunit.
Replace the @php_bin@ string in it with the
path to your PHP command-line interpreter (usually
/usr/bin/php).
Copy it to a directory that is in your PATH
and make it executable (chmod +x phpunit).
Example 4.1 shows how we have to rewrite our two tests from Example 1.4 so that we can use them with PHPUnit.
Example 4.1: Testing Array and sizeof() with PHPUnit
<?php
require_once 'PHPUnit/Framework.php';
class ArrayTest extends PHPUnit_Framework_TestCase
{
public function testNewArrayIsEmpty()
{
// Create the Array fixture.
$fixture = array();
// Assert that the size of the Array fixture is 0.
$this->assertEquals(0, sizeof($fixture));
}
public function testArrayContainsAnElement()
{
// Create the Array fixture.
$fixture = array();
// Add an element to the Array fixture.
$fixture[] = 'Element';
// Assert that the size of the Array fixture is 1.
$this->assertEquals(1, sizeof($fixture));
}
}
?>
Whenever you are tempted to type something into a
| ||
| --Martin Fowler | ||
Example 4.1 shows the basic steps for writing tests with PHPUnit:
The tests for a class Class go into a class ClassTest.
ClassTest inherits (most of the time) from PHPUnit_Framework_TestCase.
The tests are public methods that expect no parameters and are named test*.
Alternatively, you can use the @test annotation in a method's docblock to mark it as a test method.
Inside the test methods, assertion methods such as assertEquals() (see Table 20.1) are used to assert that an actual value matches an expected value.
The PHPUnit command-line test runner can be invoked through the
phpunit command. The following code shows how to run
tests with the PHPUnit command-line test runner:
phpunit ArrayTest
PHPUnit 3.1.9 by Sebastian Bergmann.
..
Time: 0 seconds
OK (2 tests)For each test run, the PHPUnit command-line tool prints one character to indicate progress:
.Printed when the test succeeds.
FPrinted when an assertion fails while running the test method.
EPrinted when an error occurs while running the test method.
SPrinted when the test has been skipped (see Chapter 9).
IPrinted when the test is marked as being incomplete or not yet implemented (see Chapter 9).
PHPUnit distinguishes between failures and
errors. A failure is a violated PHPUnit
assertion such as a failing assertEquals() call.
An error is an unexpected exception or a PHP error. Sometimes
this distinction proves useful since errors tend to be easier to fix
than failures. If you have a big list of problems, it is best to
tackle the errors first and see if you have any failures left when
they are all fixed.
Let's take a look at the command-line test runner's switches in the following code:
phpunit --help
PHPUnit 3.1.9 by Sebastian Bergmann.
Usage: phpunit [switches] UnitTest [UnitTest.php]
--log-graphviz <file> Log test execution in GraphViz markup.
--log-json <file> Log test execution in JSON format.
--log-tap <file> Log test execution in TAP format to file.
--log-xml <file> Log test execution in XML format to file.
--coverage-xml <file> Write code coverage information in XML format.
--report <dir> Generate combined test/coverage report in HTML format.
--test-db-dsn <dsn> DSN for the test database.
--test-db-log-rev <r> Revision information for database logging.
--test-db-log-info ... Additional information for database logging.
--testdox-html <file> Write agile documentation in HTML format to file.
--testdox-text <file> Write agile documentation in Text format to file.
--filter <pattern> Filter which tests to run.
--loader <loader> TestSuiteLoader implementation to use.
--repeat <times> Runs the test(s) repeatedly.
--tap Report test execution progress in TAP format.
--testdox Report test execution progress in TestDox format.
--no-syntax-check Disable syntax check of test source files.
--stop-on-failure Stop execution upon first error or failure.
--verbose Output more verbose information.
--wait Waits for a keystroke after each test.
--skeleton Generate skeleton UnitTest class for Unit in Unit.php.
--help Prints this usage information.
--version Prints the version and exits.
-d key[=value] Sets a php.ini value.phpunit UnitTest
Runs the tests that are provided by the class
UnitTest. This class is expected to be declared
in the UnitTest.php sourcefile.
UnitTest must be either a class that inherits
from PHPUnit_Framework_TestCase or a class that
provides a public static suite() method which
returns an PHPUnit_Framework_Test object, for
example an instance of the
PHPUnit_Framework_TestSuite class.
phpunit UnitTest UnitTest.php
Runs the tests that are provided by the class
UnitTest. This class is expected to be declared
in the specified sourcefile.
--log-graphviz
Generates a logfile using the GraphViz
markup for the tests run. The generated logfile can be rendered using the
dot tool, for instance.
See Chapter 15 for more details.
Please note that this parameter is only available when the
Image_GraphViz PEAR package is installed.
--log-jsonGenerates a logfile using the JSON format. See Chapter 15 for more details.
--log-tapGenerates a logfile using the Test Anything Protocol (TAP) format for the tests run. See Chapter 15 for more details.
--log-xmlGenerates a logfile in XML format for the tests run. See Chapter 15 for more details.
--coverage-xmlGenerates a logfile in XML format with the code coverage information for the tests run. See Chapter 15 for more details.
Please note that this parameter is only available when the Xdebug extension is installed.
--reportGenerates a report. When the Xdebug extension for PHP is available, the report will be a combined test result and code-coverage report. See Chapter 13 for more details.
Please note that this parameter is only available when the Xdebug extension is installed.
--test-db-*Writes test result and code coverage data to a database. See Chapter 15 for more details.
Please note that this parameter is only available when the PDO extension is installed.
--testdox-html and --testdox-textGenerates agile documentation in HTML or plain text format for the tests that are run. See Chapter 14 for more details.
--filterOnly runs tests whose name matches the given pattern. The pattern can be either the name of a single test or a regular expression that matches multiple test names.
--loader
Specifies the PHPUnit_Runner_TestSuiteLoader
implementation to use.
The standard test suite loader will look for the sourcefile in the
current working directory and in each directory that is specified in
PHP's include_path configuration directive.
Following the PEAR Naming Conventions, a class name such as
Project_Package_Class is mapped to the
sourcefile name Project/Package/Class.php.
--repeatRepeatedly runs the test(s) the specified number of times.
--stop-on-failureStop execution upon first error or failure.
--no-syntax-checkDisables the syntax check of test source files.
--tapReports the test progress using the Test Anything Protocol (TAP). See Chapter 15 for more details.
--testdoxReports the test progress as agile documentation. See Chapter 14 for more details.
--verboseOutput more verbose information, for instance the names of tests that were incomplete or have been skipped.
--waitWaits for a keystroke after each test. This is useful if you are running the tests in a window that stays open only as long as the test runner is active.
--skeleton
Generates a skeleton test-case class UnitTest
(in UnitTest.php) for a class
Unit (in Unit.php).
See Chapter 16 for more details.
-dSets the value of the given PHP configuration option.
One of the most time-consuming parts of writing tests is writing the code to set the world up in a known state and then return it to its original state when the test is complete. This known state is called the fixture of the test.
In Example 4.1, the fixture
was simply the array that is stored in the $fixture
variable. Most of the time, though, the fixture will be more complex
than a simple array, and the amount of code needed to set it up will
grow accordingly. The actual content of the test gets lost in the noise
of setting up the fixture. This problem gets even worse when you write
several tests with similar fixtures. Without some help from the testing
framework, we would have to duplicate the code that sets up the fixture
for each test we write.
PHPUnit supports sharing the setup code. Before a test method is run, a
template method called setUp() is invoked.
setUp() is where you create the objects against which
you will test. Once the test method has finished running, whether it
succeeded or failed, another template method called
tearDown() is invoked. tearDown()
is where you clean up the objects against which you tested.
We can now refactor Example 4.1 and
use setUp() to eliminate the code duplication that
we had before. First we declare the instance variable,
$fixture, that we are going to use instead of a
method-local variable. Then we put the creation of the
array fixture into the setUp()
method. Finally, we remove the redundant code from the test methods and
use the newly introduced instance variable,
$this->fixture, instead of the method-local variable
$fixture with the assertEquals()
assertion method.
Example 6.1: Using setUp() to create the Array fixture
<?php
require_once 'PHPUnit/Framework.php';
class ArrayTest extends PHPUnit_Framework_TestCase
{
protected $fixture;
protected function setUp()
{
// Create the Array fixture.
$this->fixture = array();
}
public function testNewArrayIsEmpty()
{
// Assert that the size of the Array fixture is 0.
$this->assertEquals(0, sizeof($this->fixture));
}
public function testArrayContainsAnElement()
{
// Add an element to the Array fixture.
$this->fixture[] = 'Element';
// Assert that the size of the Array fixture is 1.
$this->assertEquals(1, sizeof($this->fixture));
}
}
?>
setUp() and tearDown() will be
called once for each test method run. While it might seem frugal to
only run these methods once for all the test methods in a test-case
class, doing so would make it hard to write tests that are completely
independent of each other.
Not only are setUp() and tearDown()
run once for each test method, but the test methods are run on fresh
instances of the test-case class (see Chapter 19).
setUp() and tearDown() are nicely
symmetrical in theory but not in practice. In practice, you only need
to implement tearDown() if you have allocated
external resources like files or sockets in setUp().
If your setUp() just creates plain PHP objects, you
can generally ignore tearDown(). However, if you
create many objects in your setUp(), you might want
to unset() the variables pointing to those objects
in your tearDown() so they can be garbage collected.
The garbage collection of test case objects is not predictable.
What happens when you have two tests with slightly different setups? There are two possibilities:
If the setUp() code differs only slightly, move
the code that differs from the setUp() code to
the test method.
If you really have a different setUp(), you need
a different test-case class. Name the class after the difference in
the setup.
There are few good reasons to share fixtures between tests, but in most cases the need to share a fixture between tests stems from an unresolved design problem.
A good example of a fixture that makes sense to share across several tests is a database connection: you log into the database once and reuse the database connection instead of creating a new connection for each test. This makes your tests run faster.
Example 6.2
uses the setUp() and tearDown()
template methods of the PHPUnit_Framework_TestSuite
class (see the section called “Suite-Level Setup”) to
connect to the database before the test suite's first test and to
disconnect from the database after the last test of the test suite,
respectively. The $sharedFixture attribute of an
PHPUnit_Framework_TestSuite object is available in
the object's aggregated PHPUnit_Framework_TestSuite
and PHPUnit_Framework_TestCase objects.
Example 6.2: Sharing fixture between the tests of a test suite
<?php
require_once 'PHPUnit/Framework.php';
class DatabaseTestSuite extends PHPUnit_Framework_TestSuite
{
protected function setUp()
{
$this->sharedFixture = new PDO(
'mysql:host=wopr;dbname=test',
'root',
''
);
}
protected function tearDown()
{
$this->sharedFixture = NULL;
}
}
?>
It cannot be emphasized enough that sharing fixtures between tests reduces the value of the tests. The underlying design problem is that objects are not loosely coupled. You will achieve better results solving the underlying design problem and then writing tests using stubs (see Chapter 10), than by creating dependencies between tests at runtime and ignoring the opportunity to improve your design.
One of the goals of PHPUnit (see Chapter 2) is that tests should be composable: we want to be able to run any number or combination of tests together, for instance all tests for the whole project, or the tests for all classes of a component that is part of the project, or just the tests for a single class.
The PHPUnit_Framework_TestSuite class of the PHPUnit
framework allows us to organize tests into a hierarchy of test suites. Let
us look at PHPUnit's own test suite as an example.
Example 7.1 shows a cut-down
version of Tests/AllTests.php,
Example 7.2 a
cut-down version of Tests/Framework/AllTests.php.
Example 7.1: The AllTests class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Framework/AllTests.php';
// ...
class AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('PHPUnit');
$suite->addTest(Framework_AllTests::suite());
// ...
return $suite;
}
}
?>
Example 7.2: The Framework_AllTests class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Framework/AssertTest.php';
// ...
class Framework_AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('PHPUnit Framework');
$suite->addTestSuite('Framework_AssertTest');
// ...
return $suite;
}
}
?>
The Framework_AssertTest class is a normal test case
class that extends the PHPUnit_Framework_TestCase base
class.
Executing phpunit AllTests in the
Tests directory will run all tests.
Executing phpunit Framework_AllTests AllTests.php
in the Tests/Framework directory will run only the
tests for the PHPUnit_Framework_* classes.
Executing phpunit Framework_AssertTest AssertTest.php
in the Tests/Framework directory will run only the
tests for the PHPUnit_Framework_Assert class.
Executing phpunit --filter testFail Framework_AssertTest AssertTest.php
in the Tests/Framework directory will run only the
test named testFail from the
Framework_AssertTest class.
The PHPUnit_Framework_TestSuite class offers two
template methods, setUp() and tearDown(),
that are called before the first test of the test suite and after the last
test of the test suite, respectively.
Example 7.3: The MySuite class
<?php
require_once 'MyTest.php';
class MySuite extends PHPUnit_Framework_TestSuite
{
public static function suite()
{
return new MySuite('MyTest');
}
protected function setUp()
{
print "\nMySuite::setUp()";
}
protected function tearDown()
{
print "\nMySuite::tearDown()";
}
}
?>
The MyTest test case class that is added to the test
suite MySuite in
Example 7.3
has two test methods, testOne() and testTwo()
as well as the setUp() and tearDown()
methods. The output shows in which order these eight methods are called:
MySuite::setUp() MyTest::setUp() MyTest::testOne() MyTest::tearDown() MyTest::setUp() MyTest::testTwo() MyTest::tearDown() MySuite::tearDown()
Variables stored in $this->sharedFixture by the
setUp() method of the PHPUnit_Framework_TestSuite
class are available as $this->sharedFixture in all the
test that are aggregated by the test suite object (see
the section called “Sharing Fixture”).
Please note that a TestSuite's setUp()
and tearDown() methods will be called even if no test
of the test suite is run because it is, for instance, filtered.
PHPUnit provides extensions to the standard base-class for test classes,
PHPUnit_Framework_TestCase, that aid in the writing
of tests for exceptions, output, and performance regressions.
You can extend your test class from PHPUnit_Extensions_ExceptionTestCase
to test whether an exception is thrown inside the tested code.
Example 8.1
shows how to subclass PHPUnit_Extensions_ExceptionTestCase
and use its setExpectedException() method to set the
expected exception. If this expected exception is not thrown, the test
will be counted as a failure.
Example 8.1: Using PHPUnit_Extensions_ExceptionTestCase
<?php
require_once 'PHPUnit/Extensions/ExceptionTestCase.php';
class ExceptionTest extends PHPUnit_Extensions_ExceptionTestCase
{
public function testException()
{
$this->setExpectedException('Exception');
}
}
?>
phpunit ExceptionTest
PHPUnit 3.1.9 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testException(ExceptionTest)
Expected exception Exception
FAILURES!
Tests: 1, Failures: 1.
Table 8.1
shows the methods provided by
PHPUnit_Extensions_ExceptionTestCase.
Table 8.1. ExceptionTestCase
| Method | Meaning |
|---|---|
void setExpectedException(string $exceptionName) | Set the name of the expected exception to $exceptionName. |
String getExpectedException() | Return the name of the expected exception. |
Alternatively, you can use the approach shown in Example 8.2 to test exceptions.
Example 8.2: Alternative approach to testing exceptions
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase {
public function testException() {
try {
// ... Code that is expected to raise an Exception ...
}
catch (Exception $expected) {
return;
}
$this->fail('An expected Exception has not been raised.');
}
}
?>
If the code that is expected to raise an exception in
Example 8.2
does not raise the expected exception, the subsequent call to
fail() (see
Table 20.3) will halt the
test and signal a problem with the test. If the expected exception is
raised, the catch block will be executed, and the
test will end successfully.
Sometimes you want to assert that the execution of a method, for
instance, generates an expected output (via echo or
print, for example). The
PHPUnit_Extensions_OutputTestCase class uses PHP's
Output
Buffering feature to provide the functionality that is
necessary for this.
Example 8.3
shows how to subclass PHPUnit_Extensions_OutputTestCase
and use its expectOutputString() method to set the
expected output. If this expected output is not generated, the test
will be counted as a failure.
Example 8.3: Using PHPUnit_Extensions_OutputTestCase
<?php
require_once 'PHPUnit/Extensions/OutputTestCase.php';
class OutputTest extends PHPUnit_Extensions_OutputTestCase
{
public function testExpectFooActualFoo()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectBarActualBaz()
{
$this->expectOutputString('bar');
print 'baz';
}
}
?>
phpunit OutputTest
PHPUnit 3.1.9 by Sebastian Bergmann.
.F
Time: 0 seconds
There was 1 failure:
1) testExpectBarActualBaz(OutputTest)
Failed asserting that two strings are equal.
expected string <bar>
difference < x>
got string <baz>
FAILURES!
Tests: 2, Failures: 1.
Table 8.2
shows the methods provided by
PHPUnit_Extensions_OutputTestCase.
Table 8.2. OutputTestCase
| Method | Meaning |
|---|---|
void expectOutputRegex(string $regularExpression) | Set up the expectation that the output matches a $regularExpression. |
void expectOutputString(string $expectedString) | Set up the expectation that the output is equal to an $expectedString. |
bool setOutputCallback(callable $callback) | Sets up a callback that is used to, for instance, normalize the actual output. |
You can extend your test class from PHPUnit_Extensions_PerformanceTestCase
to test whether the execution of a function or method call, for
instance, exceeds a specified time limit.
Example 8.4
shows how to subclass PHPUnit_Extensions_PerformanceTestCase
and use its setMaxRunningTime() method to set the
maximum running time for the test. If the test is not executed within
this time limit, it will be counted as a failure.
Example 8.4: Using PHPUnit_Extensions_PerformanceTestCase
<?php
require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
class PerformanceTest extends PHPUnit_Extensions_PerformanceTestCase
{
public function testPerformance()
{
$this->setMaxRunningTime(2);
sleep(1);
}
}
?>
Table 8.3
shows the methods provided by
PHPUnit_Extensions_PerformanceTestCase.
Table 8.3. PerformanceTestCase
| Method | Meaning |
|---|---|
void setMaxRunningTime(int $maxRunningTime) | Set the maximum running time for the test to $maxRunningTime (in seconds). |
integer getMaxRunningTime() | Return the maximum running time allowed for the test. |
Chapter 17 deals with another extension, the
PHPUnit_Extensions_SeleniumTestCase class.
When you are working on a new test-case class, you might want to begin by writing empty test methods such as:
public function testSomething()
{
} to keep track of the tests that you have to write. The
problem with empty test methods is that they are interpreted as a
success by the PHPUnit framework. This misinterpretation leads to the
test reports being useless -- you cannot see whether a test is actually
successful or just not yet implemented. Calling
$this->fail() in the unimplemented test method
does not help either, since then the test will be interpreted as a
failure. This would be just as wrong as interpreting an unimplemented
test as a success.
If we think of a successful test as a green light and a test failure
as a red light, we need an additional yellow light to mark a test
as being incomplete or not yet implemented.
PHPUnit_Framework_IncompleteTest is a marker
interface for marking an exception that is raised by a test method as
the result of the test being incomplete or currently not implemented.
PHPUnit_Framework_IncompleteTestError is the
standard implementation of this interface.
Example 9.1
shows a test-case class, SampleTest, that contains one test
method, testSomething(). By calling the convenience
method markTestIncomplete() (which automatically
raises an PHPUnit_Framework_IncompleteTestError
exception) in the test method, we mark the test as being incomplete.
Example 9.1: Marking a test as incomplete
<?php
require_once 'PHPUnit/Framework.php';
class SampleTest extends PHPUnit_Framework_TestCase
{
public function testSomething()
{
// Optional: Test anything here, if you want.
$this->assertTrue(TRUE, 'This should already work.');
// Stop here and mark this test as incomplete.
$this->markTestIncomplete(
'This test has not been implemented yet.'
);
}
}
?>
An incomplete test is denoted by an I in the output
of the PHPUnit command-line test runner, as shown in the following
example:
phpunit --verbose SampleTest
PHPUnit 3.1.9 by Sebastian Bergmann.
SampleTest
I
Time: 0 seconds
There was 1 incomplete test:
1) testSomething(SampleTest)
This test has not been implemented yet.
/home/sb/SampleTest.php:14
OK, but incomplete or skipped tests!
Tests: 1, Incomplete: 1.Table 9.1 shows the API for marking tests as incomplete.
Table 9.1. API for Incomplete Tests
| Method | Meaning |
|---|---|
void markTestIncomplete() | Marks the current test as incomplete. |
void markTestIncomplete(string $message) | Marks the current test as incomplete using $message as an explanatory message. |
Not all tests can be run in every environment. Consider, for instance, a database abstraction layer that has several drivers for the different database systems it supports. The tests for the MySQL driver can of course only be run if a MySQL server is available.
Example 9.2
shows a test-case class, DatabaseTest, that contains one test
method, testConnection(). In the test-case class'
setUp() template method we check whether the MySQLi
extension is available and use the markTestSkipped()
method to skip the test if it is not.
Example 9.2: Skipping a test
<?php
require_once 'PHPUnit/Framework.php';
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!extension_loaded('mysqli')) {
$this->markTestSkipped(
'The MySQLi extension is not available.'
);
}
}
public function testConnection()
{
// ...
}
}
?>
A test that has been skipped is denoted by an S in
the output of the PHPUnit command-line test runner, as shown in the
following example:
phpunit --verbose DatabaseTest
PHPUnit 3.1.9 by Sebastian Bergmann.
DatabaseTest
S
Time: 0 seconds
There was 1 skipped test:
1) testConnection(DatabaseTest)
The MySQLi extension is not available.
/home/sb/DatabaseTest.php:11
OK, but incomplete or skipped tests!
Tests: 1, Skipped: 1.Table 9.2 shows the API for skipping tests.
Table 9.2. API for Skipping Tests
| Method | Meaning |
|---|---|
void markTestSkipped() | Marks the current test as skipped. |
void markTestSkipped(string $message) | Marks the current test as skipped using $message as an explanatory message. |
Sometimes you need to check that an object has been called correctly.
Here is an example: suppose we want to test that the correct method,
update() in our example, is called on an object that
observes another object.
In Example 10.1
we first use the getMock() method that is provided by
the PHPUnit_Framework_TestCase class (see Table 20.7) to set up a mock object for the
Observer. Since we give an array as the second
(optional) parameter for the getMock() method, only
the update() method of the Observer
class is replaced by a mock implementation.
We then use the Fluent Interface
that PHPUnit provides to specify behavior and expectations for the mock.
In essence, this means that you do not need to create several temporary
objects -- for instance one to specify that you expect the
update() method to be called and another for the
expected parameter -- and wire them together afterwards to configure
expectations. Instead, you chain method calls as shown in the example.
This leads to more readable and "fluent" code.
Example 10.1: Testing that a methods gets called once and with a specified parameter
<?php
require_once 'PHPUnit/Framework.php';
class ObserverTest extends PHPUnit_Framework_TestCase
{
public function testUpdateIsCalledOnce()
{
// Create a Mock Object for the Observer class
// mocking only the update() method.
$observer = $this->getMock('Observer', array('update'));
// Set up the expectation for the update() method
// to be called only once and with the string 'something'
// as its parameter.
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// Create a Subject object and attach the mocked
// Observer object to it.
$subject = new Subject;
$subject->attach($observer);
// Call the doSomething() method on the $subject object
// which we expect to call the mocked Observer object's
// update() method with the string 'something'.
$subject->doSomething();
}
}
?>
Table 10.1 lists the available matchers that can be used to express expectations on how often a mocked method is to be executed.
Table 10.1. Matchers
| Matcher | Meaning |
|---|---|
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() | Returns a matcher that matches when the method it is evaluated for is executed zero or more times. |
PHPUnit_Framework_MockObject_Matcher_InvokedCount never() | Returns a matcher that matches when the method it is evaluated for is never executed. |
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() | Returns a matcher that matches when the method it is evaluated for is executed at least once. |
PHPUnit_Framework_MockObject_Matcher_InvokedCount once() | Returns a matcher that matches when the method it is evaluated for is executed exactly once. |
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) | Returns a matcher that matches when the method it is evaluated for is executed exactly $count times. |
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) | Returns a matcher that matches when the method it is evaluated for is invoked at the given $index. |
The constraints that can be used together with these matchers can be found in Table 20.2.
Alternatively, you can test the Subject implementation
without the use of Mock Objects and apply the Self Shunt Pattern
instead. This uses the test-case object itself as a stub. The term
self-shunting is taken from the medical practice of installing a tube
that takes blood from an artery and returns it to a vein to provide a
convenient place for injecting drugs.
First, we make our test-case class an implementor of Observer,
an interface to be implemented by objects that want to observe
Subject:
class ObserverTest extends PHPUnit_Framework_TestCase implements Observer
{
}
Next, we implement the one Observer method,
update(), to check that it is called when the state
of the observed Subject object changes:
public $wasCalled = FALSE;
public function update(Subject $subject)
{
$this->wasCalled = TRUE;
}
Now, we can write our test. We create a new Subject
object and attach the test object to it as an observer. When the state
of the Subject changes -- for instance, by calling its
doSomething() method -- the Subject
object has to call the update() method on all objects
that are registered as observers. We use the $wasCalled
instance variable that is set by our implementation of
update() to check whether the Subject
object does what it is supposed to do:
public function testUpdate()
{
$subject = new Subject;
$subject->attach($this);
$subject->doSomething();
$this->assertTrue($this->wasCalled);
}
Notice that we create a new Subject object instead of
relying on a global instance. Stubbing encourages this style of design.
It reduces the coupling between objects and improves reuse.
If you are not familiar with the self-shunt pattern, the tests can be hard to read. What is going on here? Why is a test case also an observer? Once you get used to the idiom, the tests are easy to read. Everything you need to understand the test is in one class.
Tests that only test one thing are more informative than tests where failure can come from many sources. How can you isolate your tests from external influences? Simply put, by replacing the expensive, messy, unreliable, slow, complicated resources with stubs that are automatically generated for the purpose of your tests. For example, you can replace what is in reality a complicated computation with a constant, at least for the purposes of a single test.
Stubs solve the problem of allocating expensive external resources. For example, sharing a resource, such as a database connection, between tests helps but not using the database for the purposes of the tests at all is even better.
Example 10.2 shows how to stub method calls and set up return values.
Example 10.2: Stubbing a method call
<?php
require_once 'PHPUnit/Framework.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
$stub = $this->getMock('SomeClass', array('doSomething'));
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// Calling $stub->doSomething() will now return 'foo'.
}
}
?>
Table 10.2 lists the methods that are available to configure the return values for stubbed method calls.
Table 10.2. Stubs API
| Method | Meaning |
|---|---|
PHPUnit_Framework_MockObject_Stub_Exception throwException(Exception $exception) | Sets the exception to be thrown for all invocations of the method. |
PHPUnit_Framework_MockObject_Stub_Return returnValue(mixed $value) | Sets the return value to $value for all invocations of the method. |
PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls onConsecutiveCalls(mixed $value, ...) | Sets up different return values for consecutive invocations of the method. |
Alternatively, you can write the stub yourself and improve your design
along the way. Widely used resources are accessed through a single façade,
so you can easily replace the resource with the stub. For example,
instead of having direct database calls scattered throughout the code,
you have a single Database object, an implementor of
the IDatabase interface. Then, you can create a stub
implementation of IDatabase and use it for your
tests. You can even create an option for running the tests with the
stub database or the real database, so you can use your tests for both
local testing during development and integration testing with the real
database.
Functionality that needs to be stubbed out tends to cluster in the same object, improving cohesion. By presenting the functionality with a single, coherent interface, you reduce the coupling with the rest of the system.
You can always write more tests. However, you will quickly find that only a fraction of the tests you can imagine are actually useful. What you want is to write tests that fail even though you think they should work, or tests that succeed even though you think they should fail. Another way to think of it is in cost/benefit terms. You want to write tests that will pay you back with information. | ||
| --Erich Gamma | ||
When you need to make a change to the internal structure of the software you are working on to make it easier to understand and cheaper to modify without changing its observable behavior, a test suite is invaluable in applying these so called refactorings safely. Otherwise, you might not notice the system breaking while you are carrying out the restructuring.
The following conditions will help you to improve the code and design of your project, while using unit tests to verify that the refactoring's transformation steps are, indeed, behavior-preserving and do not introduce errors:
All unit tests run correctly.
The code communicates its design principles.
The code contains no redundancies.
The code contains the minimal number of classes and methods.
When you need to add new functionality to the system, write the tests first. Then, you will be done developing when the test runs. This practice will be discussed in detail in the next chapter.
When you get a defect report, your impulse might be to fix the defect as quickly as possible. Experience shows that this impulse will not serve you well; it is likely that the fix for the defect causes another defect.
You can hold your impulse in check by doing the following:
Verify that you can reproduce the defect.
Find the smallest-scale demonstration of the defect in the code. For example, if a number appears incorrectly in an output, find the object that is computing that number.
Write an automated test that fails now but will succeed when the defect is fixed.
Fix the defect.
Finding the smallest reliable reproduction of the defect gives you the opportunity to really examine the cause of the defect. The test you write will improve the chances that when you fix the defect, you really fix it, because the new test reduces the likelihood of undoing the fix with future code changes. All the tests you wrote before reduce the likelihood of inadvertently causing a different problem.
Unit testing offers many advantages:
Overall, integrated unit testing makes the cost and risk of any individual change smaller. It will allow the project to make [...] major architectural improvements [...] quickly and confidently. | ||
| --Benjamin Smedberg | ||
Unit Tests are a vital part of several software development practices and processes such as Test-First Programming, Extreme Programming, and Test-Driven Development. They also allow for Design-by-Contract in programming languages that do not support this methodology with language constructs.
You can use PHPUnit to write tests once you are done programming. However, the sooner a test is written after an error has been introduced, the more valuable the test is. So instead of writing tests months after the code is "complete", we can write tests days or hours or minutes after the possible introduction of a defect. Why stop there? Why not write the tests a little before the possible introduction of a defect?
Test-First Programming, which is part of Extreme Programming and Test-Driven Development, builds upon this idea and takes it to the extreme. With today's computational power, we have the opportunity to run thousands of tests thousands of times per day. We can use the feedback from all of these tests to program in small steps, each of which carries with it the assurance of a new automated test in addition to all the tests that have come before. The tests are like pitons, assuring you that, no matter what happens, once you have made progress you can only fall so far.
When you first write the test it cannot possibly run, because you are calling on objects and methods that have not been programmed yet. This might feel strange at first, but after a while you will get used to it. Think of Test-First Programming as a pragmatic approach to following the object-oriented programming principle of programming to an interface instead of programming to an implementation: while you are writing the test you are thinking about the interface of the object you are testing -- what does this object look like from the outside. When you go to make the test really work, you are thinking about pure implementation. The interface is fixed by the failing test.
The point of Test-Driven Development is to drive out the functionality the software actually needs, rather than what the programmer thinks it probably ought to have. The way it does this seems at first counterintuitive, if not downright silly, but it not only makes sense, it also quickly becomes a natural and elegant way to develop software. | ||
| --Dan North | ||
What follows is necessarily an abbreviated introduction to Test-First Programming. You can explore the topic further in other books, such as Test-Driven Development [Beck2002] by Kent Beck or Dave Astels' A Practical Guide to Test-Driven Development [Astels2003].
In this chapter, we will look at the example of a class that represents
a bank account. The contract for the BankAccount
class not only requires methods to get and set the bank account's
balance, as well as methods to deposit and withdraw money. It also
specifies the following two conditions that must be ensured:
The bank account's initial balance must be zero.
The bank account's balance cannot become negative.
Following the Test-First Programming approach, we write the tests for
the BankAccount class before we write the code for
the class itself. We use the contract conditions as the basis for the
tests and name the test methods accordingly, as shown in
Example 12.1.
Example 12.1: Tests for the BankAccount class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
protected $ba;
protected function setUp()
{
$this->ba = new BankAccount;
}
public function testBalanceIsInitiallyZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
public function testBalanceCannotBecomeNegative()
{
try {
$this->ba->withdrawMoney(1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
public function testBalanceCannotBecomeNegative2()
{
try {
$this->ba->depositMoney(-1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
}
?>
We now write the minimal amount of code needed for the first test,
testBalanceIsInitiallyZero(), to pass. In our
example this amounts to implementing the getBalance()
method of the BankAccount class, as shown in
Example 12.2.
Example 12.2: Code needed for the testBalanceIsInitiallyZero() test to pass
<?php
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
}
?>
The test for the first contract condition now passes, but the tests for the second contract condition fail because we have yet to implement the methods that these tests call.
phpunit BankAccountTest
PHPUnit 3.1.9 by Sebastian Bergmann.
.
Fatal error: Call to undefined method BankAccount::withdrawMoney()
For the tests that ensure the second contract condition to pass, we now
need to implement the withdrawMoney(),
depositMoney(), and setBalance()
methods, as shown in
Example 12.3.
These methods are written in a such a way that they raise an
BankAccountException when they are called with
illegal values that would violate the contract conditions.
Example 12.3: The complete BankAccount class
<?php
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
if ($balance >= 0) {
$this->balance = $balance;
} else {
throw new BankAccountException;
}
}
public function depositMoney($balance)
{
$this->setBalance($this->getBalance() + $balance);
return $this->getBalance();
}
public function withdrawMoney($balance)
{
$this->setBalance($this->getBalance() - $balance);
return $this->getBalance();
}
}
?>
The tests that ensure the second contract condition now pass, too:
phpunit BankAccountTest
PHPUnit 3.1.9 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests)
Alternatively, you can use the static assertion methods provided by the
PHPUnit_Framework_Assert class to write the contract
conditions as design-by-contract style assertions into your code, as
shown in Example 12.4.
When one of these assertions fails, an
PHPUnit_Framework_AssertionFailedError exception
will be raised. With this approach, you write less code for the contract
condition checks and the tests become more readable. However, you add a
runtime dependency on PHPUnit to your project.
Example 12.4: The BankAccount class with Design-by-Contract assertions
<?php
require_once 'PHPUnit/Framework.php';
class BankAccount
{
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
PHPUnit_Framework_Assert::assertTrue($balance >= 0);
$this->balance = $balance;
}
public function depositMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
$this->setBalance($this->getBalance() + $amount);
return $this->getBalance();
}
public function withdrawMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
PHPUnit_Framework_Assert::assertTrue($this->balance >= $amount);
$this->setBalance($this->getBalance() - $amount);
return $this->getBalance();
}
}
?>
By writing the contract conditions into the tests, we have used
Design-by-Contract to program the BankAccount class.
We then wrote, following the Test-First Programming approach, the
code needed to make the tests pass. However, we forgot to write
tests that call setBalance(),
depositMoney(), and withdrawMoney()
with legal values that do not violate the contract conditions.
We need a means to test our tests or at least to measure their quality.
Such a means is the analysis of code-coverage information that we will
discuss next.
You have learned how to use unit tests to test your code. But how do you test your tests? How do you find code that is not yet tested -- or, in other words, not yet covered by a test? How do you measure testing completeness? All these questions are answered by a practice called Code Coverage Analysis. Code Coverage Analysis gives you an insight into what parts of the production code are executed when the tests are run.
PHPUnit's Code Coverage Analysis utilizes the statement coverage functionality provided by the Xdebug extension. An example of what statement coverage means is that if there is a method with 100 lines of code, and only 75 of these lines are actually executed when tests are being run, then the method is considered to have a code coverage of 75 percent.
Let us generate a code coverage report for the BankAccount
class from Example 12.3.
phpunit --report ./report BankAccountTest
PHPUnit 3.1.9 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests)
Generating report, this may take a moment.Figure 13.1 shows an excerpt from a Code Coverage report. Lines of code that were executed while running the tests are highlighted green, lines of code that are executable but were not executed are highlighted red, and "dead code" is highlighted orange. The number left to the actual line of code indicates how many tests cover that line.
The code coverage report for our BankAccount example
shows that we do not have any tests yet that call the
setBalance(), depositMoney(), and
withdrawMoney() methods with legal values.
Example 13.1
shows a test that can be added to the BankAccountTest
test-case class to completely cover the BankAccount
class.
Example 13.1: Test missing to achieve complete code coverage
<?php
require_once 'PHPUnit/Framework.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
// ...
public function testDepositWithdrawMoney()
{
$this->assertEquals(0, $this->ba->getBalance());
$this->ba->depositMoney(1);
$this->assertEquals(1, $this->ba->getBalance());
$this->ba->withdrawMoney(1);
$this->assertEquals(0, $this->ba->getBalance());
}
}
?>
Figure 13.2 shows
the code coverage of the setBalance() method with the
additional test.
By default, all sourcecode files that contain at least one line of code
that has been executed (and only these files) are included in the report.
You can configure the sourcecode files that are included in the report
using the PHPUnit_Util_Filter API (see
Table 13.1).
Table 13.1. The PHPUnit_Util_Filter API
| Method | Meaning |
|---|---|
void addDirectoryToFilter(string $directory) | Adds all files from a directory that are suffixed with .php to the blacklist (recursively). |
void addDirectoryToFilter(string $directory, string $suffix) | Adds all files from a directory that are suffixed with $suffix to the blacklist (recursively). |
void addFileToFilter(string $filename) | Adds a file to the blacklist. |
void removeDirectoryFromFilter(string $directory) | Removes all files from a directory that are suffixed with .php from the blacklist (recursively). |
void removeDirectoryFromFilter(string $directory, string $suffix) | Removes all files from a directory that are suffixed with $suffix from the blacklist (recursively). |
void removeFileFromFilter(string $filename) | Removes a file from the blacklist. |
void addDirectoryToWhitelist(string $directory) | Adds all files from a directory that are suffixed with .php to the whitelist (recursively). |
void addDirectoryToWhitelist(string $directory, string $suffix) | Adds all files from a directory that are suffixed with $suffix to the whitelist (recursively). |
void addFileToWhitelist(string $filename) | Adds a file to the whitelist. |
void removeDirectoryFromWhitelist(string $directory) | Removes all files from a directory that are suffixed with .php from the whitelist (recursively). |
void removeDirectoryFromWhitelist(string $directory, string $suffix) | Removes all files from a directory that are suffixed with $suffix from the whitelist (recursively). |
void removeFileFromWhitelist(string $filename) | Removes a file from the whitelist. |
The blacklist is pre-filled with all sourcecode files of PHPUnit itself and the tests. When the whitelist is empty (default), blacklisting is used. When the whitelist is not empty, whitelisting is used. When whitelisting is used, each file on the whitelist is added to the code coverage report regardless of whether or not it was executed.
Once you get used to writing automated tests, you will likely discover more uses for tests. Here are some examples.
Typically, in a project that is developed using an agile process, such as Extreme Programming, the documentation cannot keep up with the frequent changes to the project's design and code. Extreme Programming demands collective code ownership, so all developers need to know how the entire system works. If you are disciplined enough to consequently use "speaking names" for your tests that describe what a class should do, you can use PHPUnit's TestDox functionality to generate automated documentation for your project based on its tests. This documentation gives developers an overview of what each class of the project is supposed to do.
PHPUnit's TestDox functionality looks at a test class and all the test
method names and converts them from camel case PHP names to sentences:
testBalanceIsInitiallyZero() becomes "Balance is
initially zero". If there are several test methods whose names only
differ in a suffix of one or more digits, such as
testBalanceCannotBecomeNegative() and
testBalanceCannotBecomeNegative2(), the sentence
"Balance cannot become negative" will appear only once, assuming that
all of these tests succeed.
Let us take a look at the agile documentation generated for the
BankAccount class (from
Example 12.1):
phpunit --testdox BankAccountTest
PHPUnit 3.1.9 by Sebastian Bergmann.
BankAccount
- Balance is initially zero
- Balance cannot become negative
Alternatively, the agile documentation can be generated in HTML or plain
text format and written to a file using the --testdox-html
and --testdox-text arguments.
Agile Documentation can be used to document the assumptions you make about the external packages that you use in your project. When you use an external package, you are exposed to the risks that the package will not behave as you expect, and that future versions of the package will change in subtle ways that will break your code, without you knowing it. You can address these risks by writing a test every time you make an assumption. If your test succeeds, your assumption is valid. If you document all your assumptions with tests, future releases of the external package will be no cause for concern: if the tests succeed, your system should continue working.
When you document assumptions with tests, you own the tests. The supplier of the package -- who you make assumptions about -- knows nothing about your tests. If you want to have a closer relationship with the supplier of a package, you can use the tests to communicate and coordinate your activities.
When you agree on coordinating your activities with the supplier of a package, you can write the tests together. Do this in such a way that the tests reveal as many assumptions as possible. Hidden assumptions are the death of cooperation. With the tests, you document exactly what you expect from the supplied package. The supplier will know the package is complete when all the tests run.
By using stubs (see the chapter on "Mock Objects", earlier in this book), you can further decouple yourself from the supplier: The job of the supplier is to make the tests run with the real implementation of the package. Your job is to make the tests run for your own code. Until such time as you have the real implementation of the supplied package, you use stub objects. Following this approach, the two teams can develop independently.
PHPUnit supports the logging of test results in several formats.
The XML format supported by PHPUnit is loosely based upon the one
used by the
JUnit task for Apache Ant. The following example shows the XML
logfile generated for the tests in ArrayTest:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="ArrayTest"
file="/home/sb/ArrayTest.php"
tests="2"
failures="0"
errors="0"
time="0.016030">
<testcase name="testNewArrayIsEmpty"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="6"
time="0.008044"/>
<testcase name="testArrayContainsAnElement"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="15"
time="0.007986"/>
</testsuite>
</testsuites>
The following XML logfile was generated for two tests,
testFailure and testError,
of a test-case class named FailureErrorTest and
shows how failures and errors are denoted.
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
tests="2"
failures="1"
errors="1"
time="0.019744">
<testcase name="testFailure"
class="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
line="6"
time="0.011456">
<failure type="PHPUnit_Framework_ExpectationFailedException">
testFailure(FailureErrorTest)
Failed asserting that <integer:2> matches expected value <integer:1>.
/home/sb/FailureErrorTest.php:8
</failure>
</testcase>
<testcase name="testError"
class="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
line="11"
time="0.008288">
<error type="Exception">testError(FailureErrorTest)
Exception:
/home/sb/FailureErrorTest.php:13
</error>
</testcase>
</testsuite>
</testsuites>
The XML format for code coverage information logging supported by PHPUnit
is loosely based upon the one used by
Clover. The following example shows the XML
logfile generated for the tests in BankAccountTest:
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1184835473" phpunit="3.1.9">
<project name="BankAccountTest" timestamp="1184835473">
<file name="/home/sb/BankAccount.php">
<class name="BankAccountException">
<metrics methods="0" coveredmethods="0" statements="0"
coveredstatements="0" elements="0" coveredelements="0"/>
</class>
<class name="BankAccount">
<metrics methods="4" coveredmethods="4" statements="13"
coveredstatements="5" elements="17" coveredelements="9"/>
</class>
<line num="77" type="method" count="3"/>
<line num="79" type="stmt" count="3"/>
<line num="89" type="method" count="2"/>
<line num="91" type="stmt" count="2"/>
<line num="92" type="stmt" count="0"/>
<line num="93" type="stmt" count="0"/>
<line num="94" type="stmt" count="2"/>
<line num="96" type="stmt" count="0"/>
<line num="105" type="method" count="1"/>
<line num="107" type="stmt" count="1"/>
<line num="109" type="stmt" count="0"/>
<line num="119" type="method" count="1"/>
<line num="121" type="stmt" count="1"/>
<line num="123" type="stmt" count="0"/>
<metrics loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4"
statements="13" coveredstatements="5" elements="17"
coveredelements="9"/>
</file>
<metrics files="1" loc="126" ncloc="37" classes="2" methods="4"
coveredmethods="4" statements="13" coveredstatements="5"
elements="17" coveredelements="9"/>
</project>
</coverage>
The JavaScript Object Notation (JSON)
is a lightweight data-interchange format. The following example shows
the JSON messages generated for the tests in ArrayTest:
{"event":"suiteStart","suite":"ArrayTest","tests":2}
{"event":"test","suite":"ArrayTest",
"test":"testNewArrayIsEmpty(ArrayTest)","status":"pass",
"time":0.000460147858,"trace":[],"message":""}
{"event":"test","suite":"ArrayTest",
"test":"testArrayContainsAnElement(ArrayTest)","status":"pass",
"time":0.000422954559,"trace":[],"message":""}
The following JSON messages were generated for two tests,
testFailure and testError,
of a test-case class named FailureErrorTest and
show how failures and errors are denoted.
{"event":"suiteStart","suite":"FailureErrorTest","tests":2}
{"event":"test","suite":"FailureErrorTest",
"test":"testFailure(FailureErrorTest)","status":"fail",
"time":0.000483989716,"trace":[],"message":""}
{"event":"test","suite":"FailureErrorTest",
"test":"testError(FailureErrorTest)","status":"error",
"time":0.000466108322,"trace":[],"message":""}
The Test Anything Protocol (TAP)
is Perl's simple text-based interface between testing modules. The
following example shows the TAP logfile generated for the tests in
ArrayTest:
1..2 # TestSuite "ArrayTest" started. ok 1 - testNewArrayIsEmpty(ArrayTest) ok 2 - testArrayContainsAnElement(ArrayTest) # TestSuite "ArrayTest" ended.
The following TAP logfile was generated for two tests,
testFailure and testError,
of a test-case class named FailureErrorTest and
shows how failures and errors are denoted.
1..2 # TestSuite "FailureErrorTest" started. not ok 1 - Failure: testFailure(FailureErrorTest) not ok 2 - Error: testError(FailureErrorTest) # TestSuite "FailureErrorTest" ended.
PHPUnit can generate a description of the test result as a graph that can be rendered to diagrams in several useful formats such as images using the GraphViz tools.
phpunit --log-graphviz BankAccount.dot BankAccountTest
PHPUnit 3.1.9 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests)
The following example shows the GraphViz markup generated (and saved
to the BankAccount.dot file in the current directory)
for the tests in BankAccountTest:
digraph G {
graph [ overlap="scale",splines="true",sep=".1",fontsize="8" ];
"BankAccountTest" [ color="green" ];
subgraph "cluster_BankAccountTest" {
label="";
"testBalanceIsInitiallyZero" [ color="green" ];
"testBalanceCannotBecomeNegative" [ color="green" ];
"testBalanceCannotBecomeNegative2" [ color="green" ];
}
"BankAccountTest" -> "testBalanceIsInitiallyZero";
"BankAccountTest" -> "testBalanceCannotBecomeNegative";
"BankAccountTest" -> "testBalanceCannotBecomeNegative2";
}
We can now use the dot command-line tool that is
part of the GraphViz software suite to generate a graphic from this
markup:
dot -T png -o BankAccount.png BankAccount.dotFigure 15.1 shows the graph representation of the test result rendered from the GraphViz markup above.
Successful tests are displayed with a green border, failures and errors with a red border, and incomplete or skipped tests with a yellow border. A parent node, such as test suite, is displayed with a non-green border if it has a child node, such as a test, that was not successful.
PHPUnit can write test result and code coverage data to a test database. Several ideas for future features depend on this data.
For each run of the test suite there is a row in the run table.
For each test that is executed as part of a test run there is a row in the test table.
For each file that is part of a revision there is a row in the code_file table.
For each class that is declared in a file that is part of a revision there is a row in the code_class table.
For each method that is declared in a file that is part of a revision there is a row in the code_method table.
For each line of code that belongs to a file that is part of a revision there is a row in the code_line table.
The code_coverage table connects each test to the lines of code it covers.
Before we can write test result and code coverage data to a database, we need to create a database using one of the supplied schema definitions:
sqlite3 BankAccount.db < PHPUnit/Util/Log/Database/SQLite3.sqlNow we can execute a test suite and write the test result and code coverage data to a database:
phpunit --test-db-dsn sqlite:///home/sb/BankAccount.db --test-db-log-rev 1 BankAccountTest
PHPUnit 3.1.9 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests)
Storing code coverage data in database, this may take a moment.Table 15.1 shows the arguments understood by the TextUI test runner (see Chapter 5) for the test database.
Table 15.1. TextUI Arguments for the Test Database
| Argument | Meaning |
|---|---|
--test-db-dsn <dsn> | The PDO data source name (DSN) for the database connection. In general, a DSN consists of the PDO driver name, followed by a colon, followed by the PDO driver-specific connection syntax. |
--test-db-log-rev <r> | A number, for instance a Subversion Global Revision Number, that uniquely identifies the current revision of the codebase. |
--test-db-log-info ... | Additional information on the test environment, for instance. |
When you are writing tests for existing code, you have to write the same code fragments such as
public function testMethod()
{
}over and over again. PHPUnit can help you by analyzing the code of the existing class and generating a skeleton test-case class for it.
Example 16.1: The Calculator class
<?php
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
}
?>
The following example shows how to generate a skeleton test class
for a class named Calculator
(see Example 16.1).
phpunit --skeleton Calculator
PHPUnit 3.1.9 by Sebastian Bergmann.
Wrote test class skeleton for Calculator to CalculatorTest.php.For each method in the original class, there will be an incomplete test-case (see Chapter 9) in the generated test-case class.
Below is the output of running the generated test-case class.
phpunit --verbose CalculatorTest
PHPUnit 3.1.9 by Sebastian Bergmann.
CalculatorTest
I
Time: 0 seconds
There was 1 incomplete test:
1) testAdd(CalculatorTest)
This test has not been implemented yet.
/home/sb/CalculatorTest.php:54
OK, but incomplete or skipped tests!
Tests: 1, Incomplete: 1.
You can use @assert annotation in the
documentation block of a method to automatically generate simple,
yet meaningful tests instead of incomplete test-cases.
Example 16.2
shows an example.
Example 16.2: The Calculator class with @assert annotations
<?php
class Calculator
{
/**
* @assert (0, 0) == 0
* @assert (0, 1) == 1
* @assert (1, 0) == 1
* @assert (1, 1) == 2
*/
public function add($a, $b)
{
return $a + $b;
}
}
?>
Each method in the original class is checked for @assert
annotations. These are transformed into test code such as
/**
* Generated from @assert (0, 0) == 0.
*/
public function testAdd() {
$o = new Calculator;
$this->assertEquals(0, $o->add(0, 0));
}
Below is the output of running the generated test-case class.
phpunit CalculatorTest
PHPUnit 3.1.9 by Sebastian Bergmann.
....
Time: 0 seconds
OK (4 tests)
Table 16.1
shows the supported variations of the @assert
annotation and how they are transformed into test code.
Table 16.1. Supported variations of the @assert annotation
| Annotation | Transformed to |
|---|---|
@assert (...) == X | assertEquals(X, method(...)) |
@assert (...) != X | assertNotEquals(X, method(...)) |
@assert (...) === X | assertSame(X, method(...)) |
@assert (...) !== X | assertNotSame(X, method(...)) |
@assert (...) > X | assertThat(method(...), greaterThan(X)) |
@assert (...) >= X | assertThat(method(...), greaterThanOrEqual(X)) |
@assert (...) < X | assertThat(method(...), lessThan(X)) |
@assert (...) <= X | assertThat(method(...), lessThanOrEqual(X)) |
Selenium RC is a test tool that allows you to write automated user-interface tests for web applications in any programming language against any HTTP website using any mainstream browser. It uses Selenium Core, a library that performs automated browser tasks using JavaScript. Selenium tests run directly in a browser, just as real users do. These tests can be used for both acceptance testing (by performing higher-level tests on the integrated system instead of just testing each unit of the system independently) and browser compatibility testing (by testing the web application on different operating systems and browsers).
Let us take a look at how Selenium RC is installed:
server/selenium-server.jar to /usr/local/bin, for instance.java -jar /usr/local/bin/selenium-server.jar.Now we can send commands to the Selenium RC server using its client/server protocol.
The PHPUnit_Extensions_SeleniumTestCase test case
extension implements the client/server protocol to talk to Selenium RC as
well as specialized assertion methods for web testing.
Example 17.1 shows
how to test the contents of the <title>
element of the http://www.example.com/
website.
Example 17.1: Usage example for PHPUnit_Extensions_SeleniumTestCase
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitleEquals('Example Web Page');
}
}
?>
Unlike with the PHPUnit_Framework_TestCase class,
test case classes that extend PHPUnit_Extensions_SeleniumTestCase
have to provide a setUp() method. This method is used
to configure the Selenium RC session. See
Table 17.1
for the list of methods that are available for this.
Table 17.1. Selenium RC API: Setup
| Method | Meaning |
|---|---|
void setBrowser(string $browser) | Set the browser to be used by the Selenium RC server. |
void setBrowserUrl(string $browserUrl) | Set the base URL for the tests. |
void setHost(string $host) | Set the hostname for the connection to the Selenium RC server. |
void setPort(int $port) | Set the port for the connection to the Selenium RC server. |
void setTimeout(int $timeout) | Set the timeout for the connection to the Selenium RC server. |
void setSleep(int $seconds) | Set the number of seconds the Selenium RC client should sleep between sending action commands to the Selenium RC server. |
Table 17.2 lists the
various assertion methods that PHPUnit_Extensions_SeleniumTestCase
provides.
Table 17.2. Assertions
| Assertion | Meaning |
|---|---|
void assertAlertPresent() | Reports an error if no alert is present. |
void assertNoAlertPresent() | Reports an error if an alert is present. |
void assertChecked(string $locator) | Reports an error if the element identified by $locator is not checked. |
void assertNotChecked(string $locator) | Reports an error if the element identified by $locator is checked. |
void assertConfirmationPresent() | Reports an error if no confirmation is present. |
void assertNoConfirmationPresent() | Reports an error if a confirmation is present. |
void assertEditable(string $locator) | Reports an error if the element identified by $locator is not editable. |
void assertNotEditable(string $locator) | Reports an error if the element identified by $locator is editable. |
void assertElementValueEquals(string $locator, string $text) | Reports an error if the value of the element identified by $locator is not equal to the given $text. |
void assertElementValueNotEquals(string $locator, string $text) | Reports an error if the value of the element identified by $locator is equal to the given $text. |
void assertElementContainsText(string $locator, string $text) | Reports an error if the element identified by $locator does not contain the given $text. |
void assertElementNotContainsText(string $locator, string $text) | Reports an error if the element identified by $locator contains the given $text. |
void assertElementPresent(string $locator) | Reports an error if the element identified by $locator is not present. |
void assertElementNotPresent(string $locator) | Reports an error if the element identified by $locator is present. |
void assertLocationEquals(string $location) | Reports an error if the current location is not equal to the given $location. |
void assertLocationNotEquals(string $location) | Reports an error if the current location is equal to the given $location. |
void assertPromptPresent() | Reports an error if no prompt is present. |
void assertNoPromptPresent() | Reports an error if a prompt is present. |
void assertIsSelected(string $selectLocator, string $value) | Reports an error if the given value is not selected. |
void assertIsNotSelected(string $selectLocator, string $value) | Reports an error if the given value is selected. |
void assertSomethingSelected(string $selectLocator) | Reports an error if the option identified by $selectLocator is not selected. |
void assertNothingSelected(string $selectLocator) | Reports an error if the option identified by $selectLocator is selected. |
void assertTextPresent(string $pattern) | Reports an error if the given $pattern is not present. |
void assertTextNotPresent(string $pattern) | Reports an error if the given $pattern is present. |
void assertTitleEquals(string $title) | Reports an error if the current title is not equal to the given $title. |
void assertTitleNotEquals(string $title) | Reports an error if the current title is equal to the given $title. |
void assertVisible(string $locator) | Reports an error if the element identified by $locator is not visible. |
void assertNotVisible(string $locator) | Reports an error if the element identified by $locator is visible. |
Table 17.3 shows
the two template methods of PHPUnit_Extensions_SeleniumTestCase:
Table 17.3. Template Methods
| Method | Meaning |
|---|---|
void defaultAssertions() | Override to perform assertions that are shared by all tests of a test case. This method is called after each command that is sent to the Selenium RC server. |
void sharedAssertions() | Override to perform assertions that are shared by all tests of a test case. This method is called before the execution of a test ends. |
Please refer to the documentation of Selenium Core for a reference of the commands available and how they are used.
Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily, leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly. | ||
| --Martin Fowler | ||
This chapter provides an overview of Continuous Integration summarizing the technique and its application with PHPUnit.
Continuous Integration demands a fully automated and reproducible build, including testing, that runs many times a day. This allows each developer to integrate daily thus reducing integration problems. While this can be achieved by setting up a cronjob that makes a fresh checkout from the project's source code repository at regular intervals, runs the tests, and publishes the results a more comfortable solution may be desired.
This is where a framework for continuous build processes such as CruiseControl comes into play. It includes, but is not limited to, plugins for email notification, Apache Ant, and various source control tools. A web interface is provided to view the details of the current and previous builds.
The following example assumes that CruiseControl has been installed into
/usr/local/cruisecontrol.
cd /usr/local/cruisecontrolmkdir -p projects/BankAccount/build/logscd projects/BankAccountsvn co svn://svn.phpunit.de/phpunit/phpunit/branches/release/3.1/PHPUnit/Samples/BankAccount sourcebuild.xml file.Example 18.1: projects/BankAccount/build.xml
<project name="BankAccount" default="build" basedir=".">
<target name="checkout">
<exec dir="${basedir}/source/" executable="svn">
<arg line="up"/>
</exec>
</target>
<target name="test">
<exec dir="${basedir}/source" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/bankaccount.xml BankAccountTest"/>
</exec>
</target>
<target name="build" depends="checkout,test"/>
</project>cd /usr/local/cruisecontrolconfig.xml file.Example 18.2: config.xml
<cruisecontrol>
<project name="BankAccount" buildafterfailed="false">
<plugin
name="svnbootstrapper"
classname="net.sourceforge.cruisecontrol.bootstrappers.SVNBootstrapper"/>
<plugin
name="svn"
classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN"/>
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<bootstrappers>
<svnbootstrapper localWorkingCopy="projects/${project.name}/source/"/>
</bootstrappers>
<modificationset>
<svn localWorkingCopy="projects/${project.name}/source/"/>
</modificationset>
<schedule interval="300">
<ant
anthome="apache-ant-1.7.0"
buildfile="projects/${project.name}/build.xml"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<currentbuildstatuspublisher
file="logs/${project.name}/buildstatus.txt"/>
<email
mailhost="localhost"
buildresultsurl="http://cruise.example.com/buildresults/${project.name}"
skipusers="true"
spamwhilebroken="true"
returnaddress="project@example.com">
<failure address="dev@lists.example.com" reportWhenFixed="true"/>
</email>
</publishers>
</project>
</cruisecontrol>./cruisecontrol.shhttp://localhost:8080/ in your webbrowser.Apache Maven is a software project management and comprehension tool. Based on the concept of a Project Object Model (POM), Apache Maven can manage a project's build, reporting and documentation from a central place of information.
The single XML logfile that PHPUnit's XML logging facility (see the section called “XML Format”) generates needs to be split into separate XML
logfiles, one for each test suite, before it can be processed by Apache
Maven's surefire plugin.
This plugin is used during the test phase of the build lifecycle to
execute the unit tests of an application. Example 18.4
shows an XSLT stylesheet that performs this splitting. Example 18.3 shows an
example pom.xml configuration file.
Example 18.3: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ... -->
<prerequisites>
<maven>2.0.7</maven>
</prerequisites>
<!-- ... -->
<build>
<!-- ... -->
<plugins>
<plugin>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-trax</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>saxon</artifactId>
<version>8.7</version>
</dependency>
</dependencies>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.2-SNAPSHOT</version>
<executions>
<execution>
<id>codecoverage</id>
<phase>pre-site</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<property name="phpunit.codecoverage"
location="${project.reporting.outputDirectory}/phpunit/codecoverage" />
<property name="surefire.reports"
location="${project.build.directory}/surefire-reports" />
<mkdir dir="${phpunit.codecoverage}"/>
<mkdir dir="${surefire.reports}"/>
<!-- ${ant.phpunit} path to PHPUnit executable -->
<!-- ${ant.pear};... this is the include path for your test execution -->
<!-- ${test.AllTests} PHPUnit cmd line args like 'AllTests de/dmc/dashboard/AllTests.php' -->
<exec executable="${ant.phpunit}" dir="${basedir}">
<arg line="-d include_path=${ant.pear};${project.build.sourceDirectory};${project.build.testSourceDirectory}
--report ${phpunit.codecoverage} ${test.AllTests}" />
</exec>
<xslt in="${phpunit.codecoverage}/logfile.xml"
out="${surefire.reports}/xslt.info"
style="src/test/config/phpunit_to_surefire.xslt"
processor="trax">
<!-- this is the output folder for surefire like XML Reports -->
<param name="outputDir" expression="${surefire.reports}"/>
</xslt>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.4-SNAPSHOT</version>
<reportSets>
<reportSet>
<reports>
<report>report-only</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
</project>Example 18.4: phpunit_to_surefire.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="outputDir">.</xsl:param>
<xsl:template match="testsuites">
<xsl:apply-templates select="testsuite"/>
</xsl:template>
<xsl:template match="testsuite">
<xsl:if test="testcase">
<xsl:variable name="outputName" select="./@name"/>
<xsl:result-document href="file:///{$outputDir}/{$outputName}.xml" method="xml">
<xsl:copy-of select="."/>
</xsl:result-document>
</xsl:if>
<xsl:apply-templates select="testsuite"/>
</xsl:template>
</xsl:stylesheet>
The implementation of PHPUnit is a bit unusual, using techniques that are difficult to maintain in ordinary application code. Understanding how PHPUnit runs your tests can help you write them.
A single test is represented by a PHPUnit_Framework_Test
object and requires a PHPUnit_Framework_TestResult
object to be run. The PHPUnit_Framework_TestResult
object is passed to the PHPUnit_Framework_Test
object's run() method, which runs the actual test
method and reports any exceptions to the
PHPUnit_Framework_TestResult object. This is an
idiom from the Smalltalk world called Collecting Parameter.
It suggests that when you need to collect results over several methods
(in our case the result of the serveral invocations of the
run() method for the various tests), you should add a
parameter to the method and pass an object that will collect the results
for you. See the article "JUnit: A
Cook's Tour" by Erich Gamma and Kent Beck [GammaBeck1999]
and "Smalltalk Best Practice Patterns" by Kent Beck [Beck1997].
To further understand how PHPUnit runs your tests, consider the test-case class in Example 19.1.
Example 19.1: The EmptyTest class
<?php
require_once 'PHPUnit/Framework.php';
class EmptyTest extends PHPUnit_Framework_TestCase
{
private $emptyArray = array();
public function testSize()
{
$this->assertEquals(0, sizeof($this->emptyArray));
}
public function testIsEmpty()
{
$this->assertTrue(empty($this->emptyArray));
}
}
?>
When the test is run, the first thing PHPUnit does is convert the
test class into a PHPUnit_Framework_Test object --
here, a PHPUnit_Framework_TestSuite containing
two instances of EmptyTest, as shown in
Figure 19.1.
When the PHPUnit_Framework_TestSuite is run, it runs
each of the EmptyTests in turn. Each runs its own
setUp() method, creating a fresh
$emptyArray for each test, as shown in
Figure 19.2. This way,
if one test modifies the array, the other test will not be affected. Even
changes to global and super-global variables (such as
$GLOBALS) do not affect other tests.
The implementation of the backup and restore operations for the
global and super-global variables uses serialize() and
unserialize(). Objects of some classes that are
provided by PHP itself, such as PDO, cannot be
serialized and the backup operation will break when such an object is
stored in the $GLOBALS array, for instance.
The backup and restore operations for the global and super-global variables can be disabled like this
class MyTest extends PHPUnit_Framework_TestCase
{
protected $backupGlobals = FALSE;
// ...
}
Please note that setting the $backupGlobals attribute
inside the setUp() method, for instance, has no effect.
In short, one test-case class results in a two-level tree of objects
when the tests are run. Each test method works with its own copy of the
objects created by setUp(). The result is tests that
can run completely independently.
To run the test method itself, PHPUnit uses reflection to find the
method name in the instance variable $name and invokes
it. This is another idiom, called Pluggable Selector,
that is commonly used in the Smalltalk world. Using a Pluggable Selector
makes the writing of tests simpler, but there is a tradeoff: you cannot
look at the code to decide whether a method is invoked, you have to look
at the data values at runtime.
For most uses, PHPUnit has a simple API: subclass
PHPUnit_Framework_TestCase for your test cases and
call assertTrue() or assertEquals().
However, for those of you who would like to look deeper into PHPUnit,
here are all of its published methods and classes.
Most of the time, you will encounter five classes or interfaces when you are using PHPUnit:
PHPUnit_Framework_AssertA collection of static methods for checking actual values against expected values.
PHPUnit_Framework_TestThe interface of all objects that act like tests.
PHPUnit_Framework_TestCaseA single test.
PHPUnit_Framework_TestSuiteA collection of tests.
PHPUnit_Framework_TestResultA summary of the results of running one or more tests.
Figure 20.1
shows the relationship of the five basic classes and interfaces
in PHPUnit: PHPUnit_Framework_Assert,
PHPUnit_Framework_Test,
PHPUnit_Framework_TestCase,
PHPUnit_Framework_TestSuite, and
PHPUnit_Framework_TestResult.
Most test cases written for PHPUnit are derived indirectly from the class
PHPUnit_Framework_Assert, which contains methods for
automatically checking values and reporting discrepancies. The methods
are declared static, so you can write design-by-contract style
assertions in your methods and have them reported through PHPUnit
(Example 20.1).
Example 20.1: Design-by-Contract Style Assertions
<?php
require_once 'PHPUnit/Framework.php';
class Sample
{
public function aSampleMethod($object)
{
PHPUnit_Framework_Assert::assertNotNull($object);
}
}
$sample = new Sample;
$sample->aSampleMethod(NULL);
?>
Fatal error: Uncaught exception 'PHPUnit_Framework_ExpectationFailedException' with message 'Failed asserting that <null> is not identical to <null>.'
Most of the time, though, you'll be checking the assertions inside of tests.
There are two variants of each of the assertion methods: one takes a message to be displayed with the error as a parameter, and one does not. The optional message is typically displayed when a failure is displayed, which can make debugging easier.
Example 20.2: Using assertions with messages
<?php
require_once 'PHPUnit/Framework.php';
class MessageTest extends PHPUnit_Framework_TestCase
{
public function testMessage()
{
$this->assertTrue(FALSE, 'This is a custom message.');
}
}
?>
The following example shows the output you get when you run the
testMessage() test from
Example 20.2, using
assertions with messages:
phpunit MessageTest
PHPUnit 3.1.6 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testMessage(MessageTest)
This is a custom message.
Failed asserting that <boolean:false> is true.
/home/sb/MessageTest.php:8
FAILURES!
Tests: 1, Failures: 1.Table 20.1 shows all the varieties of assertions.
Table 20.1. Assertions
| Assertion | Meaning |
|---|---|
void assertTrue(bool $condition) | Reports an error if $condition is FALSE. |
void assertTrue(bool $condition, string $message) | Reports an error identified by $message if $condition is FALSE. |
void assertFalse(bool $condition) | Reports an error if $condition is TRUE. |
void assertFalse(bool $condition, string $message) | Reports an error identified by $message if $condition is TRUE. |
void assertNull(mixed $variable) | Reports an error if $variable is not NULL. |
void assertNull(mixed $variable, string $message) | Reports an error identified by $message if $variable is not NULL. |
void assertNotNull(mixed $variable) | Reports an error if $variable is NULL. |
void assertNotNull(mixed $variable, string $message) | Reports an error identified by $message if $variable is NULL. |
void assertSame(object $expected, object $actual) | Reports an error if the two variables $expected and $actual do not reference the same object. |
void assertSame(object $expected, object $actual, string $message) | Reports an error identified by $message if the two variables $expected and $actual do not reference the same object. |
void assertSame(mixed $expected, mixed $actual) | Reports an error if the two variables $expected and $actual do not have the same type and value. |
void assertSame(mixed $expected, mixed $actual, string $message) | Reports an error identified by $message if the two variables $expected and $actual do not have the same type and value. |
void assertNotSame(object $expected, object $actual) | Reports an error if the two variables $expected and $actual reference the same object. |
void assertNotSame(object $expected, object $actual, string $message) | Reports an error identified by $message if the two variables $expected and $actual reference the same object. |
void assertNotSame(mixed $expected, mixed $actual) | Reports an error if the two variables $expected and $actual have the same type and value. |
void assertNotSame(mixed $expected, mixed $actual, string $message) | Reports an error identified by $message if the two variables $expected and $actual have the same type and value. |
void assertAttributeSame(object $expected, string $actualAttributeName, string $actualClassName) | Reports an error if $actualClassName::$actualAttributeName and $actual do not reference the same object. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(object $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if $actualClassName::$actualAttributeName and $actual do not reference the same object. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(mixed $expected, string $actualAttributeName, string $actualClassName) | Reports an error if $actualClassName::$actualAttributeName and $actual do not have the same type and value. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(mixed $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if $actualClassName::$actualAttributeName and $actual do not have the same type and value. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(object $expected, string $actualAttributeName, string $actualClassName) | Reports an error if $actualClassName::$actualAttributeName and $actual reference the same object. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(object $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if $actualClassName::$actualAttributeName and $actual reference the same object. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, string $actualClassName) | Reports an error if $actualClassName::$actualAttributeName and $actual have the same type and value. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if $actualClassName::$actualAttributeName and $actual have the same type and value. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(object $expected, string $actualAttributeName, object $actualObject) | Reports an error if $actualObject->actualAttributeName and $actual do not reference the same object. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(object $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if $actualObject->actualAttributeName and $actual do not reference the same object. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(mixed $expected, string $actualAttributeName, object $actualObject) | Reports an error if $actualObject->actualAttributeName and $actual do not have the same type and value. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeSame(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if $actualObject->actualAttributeName and $actual do not have the same type and value. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(object $expected, string $actualAttributeName, object $actualObject) | Reports an error if $actualObject->actualAttributeName and $actual reference the same object. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(object $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if $actualObject->actualAttributeName and $actual reference the same object. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, object $actualObject) | Reports an error if $actualObject->actualAttributeName and $actual have the same type and value. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if $actualObject->actualAttributeName and $actual have the same type and value. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertEquals(array $expected, array $actual) | Reports an error if the two arrays $expected and $actual are not equal. |
void assertEquals(array $expected, array $actual, string $message) | Reports an error identified by $message if the two arrays $expected and $actual are not equal. |
void assertNotEquals(array $expected, array $actual) | Reports an error if the two arrays $expected and $actual are equal. |
void assertNotEquals(array $expected, array $actual, string $message) | Reports an error identified by $message if the two arrays $expected and $actual are equal. |
void assertEquals(float $expected, float $actual, '', float $delta = 0) | Reports an error if the two floats $expected and $actual are not within $delta of each other. |
void assertEquals(float $expected, float $actual, string $message, float $delta = 0) | Reports an error identified by $message if the two floats $expected and $actual are not within $delta of each other. |
void assertNotEquals(float $expected, float $actual, '', float $delta = 0) | Reports an error if the two floats $expected and $actual are within $delta of each other. |
void assertNotEquals(float $expected, float $actual, string $message, float $delta = 0) | Reports an error identified by $message if the two floats $expected and $actual are within $delta of each other. |
void assertEquals(object $expected, object $actual) | Reports an error if the two objects $expected and $actual do not have equal attribute values. |
void assertEquals(object $expected, object $actual, string $message) | Reports an error identified by $message if the two objects $expected and $actual do not have equal attribute values. |
void assertNotEquals(object $expected, object $actual) | Reports an error if the two objects $expected and $actual have equal attribute values. |
void assertNotEquals(object $expected, object $actual, string $message) | Reports an error identified by $message if the two objects $expected and $actual have equal attribute values. |
void assertEquals(string $expected, string $actual) | Reports an error if the two strings $expected and $actual are not equal. The error is reported as the delta between the two strings. |
void assertEquals(string $expected, string $actual, string $message) | Reports an error identified by $message if the two strings $expected and $actual are not equal. The error is reported as the delta between the two strings. |
void assertNotEquals(string $expected, string $actual) | Reports an error if the two strings $expected and $actual are equal. |
void assertNotEquals(string $expected, string $actual, string $message) | Reports an error identified by $message if the two strings $expected and $actual are equal. |
void assertEquals(DOMDocument $expected, DOMDocument $actual) | Reports an error if the XML documents represented by the two DOMDocument objects $expected and $actual are not equal. |
void assertEquals(DOMDocument $expected, DOMDocument $actual, string $message) | Reports an error identified by $message if the XML documents represented by the two DOMDocument objects $expected and $actual are not equal. |
void assertNotEquals(DOMDocument $expected, DOMDocument $actual) | Reports an error if the XML documents represented by the two DOMDocument objects $expected and $actual are equal. |
void assertNotEquals(DOMDocument $expected, DOMDocument $actual, string $message) | Reports an error identified by $message if the XML documents represented by the two DOMDocument objects $expected and $actual are equal. |
void assertEquals(mixed $expected, mixed $actual) | Reports an error if the two variables $expected and $actual are not equal. |
void assertEquals(mixed $expected, mixed $actual, string $message) | Reports an error identified by $message if the two variables $expected and $actual are not equal. |
void assertNotEquals(mixed $expected, mixed $actual) | Reports an error if the two variables $expected and $actual are equal. |
void assertNotEquals(mixed $expected, mixed $actual, string $message) | Reports an error identified by $message if the two variables $expected and $actual are equal. |
void assertAttributeEquals(array $expected, string $actualAttributeName, string $actualClassName) | Reports an error if the two arrays $expected and $actualClassName::$actualAttributeName are not equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(array $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if the two arrays $expected and $actualClassName::$actualAttributeName are not equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, string $actualClassName) | Reports an error if the two arrays $expected and $actualClassName::$actualAttributeName are equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if the two arrays $expected and $actualClassName::$actualAttributeName are equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(float $expected, string $actualAttributeName, string $actualClassName, '', float $delta = 0) | Reports an error if the two floats $expected and $actualClassName::$actualAttributeName are not within $delta of each other. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(float $expected, string $actualAttributeName, string $actualClassName, string $message, float $delta = 0) | Reports an error identified by $message if the two floats $expected and $actualClassName::$actualAttributeName are not within $delta of each other. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, string $actualClassName, '', float $delta = 0) | Reports an error if the two floats $expected and $actualClassName::$actualAttributeName are within $delta of each other. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, string $actualClassName, string $message, float $delta = 0) | Reports an error identified by $message if the two floats $expected and $actualClassName::$actualAttributeName are within $delta of each other. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(string $expected, string $actualAttributeName, string $actualClassName) | Reports an error if the two strings $expected and $actualClassName::$actualAttributeName are not equal. The error is reported as the delta between the two strings. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(string $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if the two strings $expected and $actualClassName::$actualAttributeName are not equal. The error is reported as the delta between the two strings. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, string $actualClassName) | Reports an error if the two strings $expected and $actualClassName::$actualAttributeName are equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if the two strings $expected and $actualClassName::$actualAttributeName are equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, string $actualClassName) | Reports an error if the two variables $expected and $actualClassName::$actualAttributeName are not equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if the two variables $expected and $actualClassName::$actualAttributeName are not equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, string $actualClassName) | Reports an error if the two variables $expected and $actualClassName::$actualAttributeName are equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, string $actualClassName, string $message) | Reports an error identified by $message if the two variables $expected and $actualClassName::$actualAttributeName are equal. The visibility of the $actualClassName::$actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(array $expected, string $actualAttributeName, object $actualObject) | Reports an error if the two arrays $expected and $actualObject->actualAttributeName are not equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(array $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if the two arrays $expected and $actualObject->actualAttributeName are not equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, object $actualObject) | Reports an error if the two arrays $expected and $actualObject->actualAttributeName are equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if the two arrays $expected and $actualObject->actualAttributeName are equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(float $expected, string $actualAttributeName, object $actualObject, '', float $delta = 0) | Reports an error if the two floats $expected and $actualObject->actualAttributeName are not within $delta of each other. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(float $expected, string $actualAttributeName, object $actualObject, string $message, float $delta = 0) | Reports an error identified by $message if the two floats $expected and $actualObject->actualAttributeName are not within $delta of each other. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, object $actualObject, '', float $delta = 0) | Reports an error if the two floats $expected and $actualObject->actualAttributeName are within $delta of each other. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, object $actualObject, string $message, float $delta = 0) | Reports an error identified by $message if the two floats $expected and $actualObject->actualAttributeName are within $delta of each other. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(string $expected, string $actualAttributeName, object $actualObject) | Reports an error if the two strings $expected and $actualObject->actualAttributeName are not equal. The error is reported as the delta between the two strings. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(string $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if the two strings $expected and $actualObject->actualAttributeName are not equal. The error is reported as the delta between the two strings. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, object $actualObject) | Reports an error if the two strings $expected and $actualObject->actualAttributeName are equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if the two strings $expected and $actualObject->actualAttributeName are equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, object $actualObject) | Reports an error if the two variables $expected and $actualObject->actualAttributeName are not equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if the two variables $expected and $actualObject->actualAttributeName are not equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, object $actualObject) | Reports an error if the two variables $expected and $actualObject->actualAttributeName are equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | Reports an error identified by $message if the two variables $expected and $actualObject->actualAttributeName are equal. The visibility of the $actualObject->actualAttributeName attribute may be public, protected, or private. |
void assertContains(mixed $needle, array $haystack) | Reports an error if $needle is not an element of $haystack. |
void assertContains(mixed $needle, array $haystack, string $message) | Reports an error identified by $message if $needle is not an element of $haystack. |
void assertNotContains(mixed $needle, array $haystack) | Reports an error if $needle is an element of $haystack. |
void assertNotContains(mixed $needle, array $haystack, string $message) | Reports an error identified by $message if $needle is an element of $haystack. |
void assertContains(mixed $needle, Iterator $haystack) | Reports an error if $needle is not an element of $haystack. |
void assertContains(mixed $needle, Iterator $haystack, string $message) | Reports an error identified by $message if $needle is not an element of $haystack. |
void assertNotContains(mixed $needle, Iterator $haystack) | Reports an error if $needle is an element of $haystack. |
void assertNotContains(mixed $needle, Iterator $haystack, string $message) | Reports an error identified by $message if $needle is an element of $haystack. |
void assertContains(string $needle, string $haystack) | Reports an error if $needle is not a substring of $haystack. |
void assertContains(string $needle, string $haystack, string $message) | Reports an error identified by $message if $needle is not a substring of $haystack. |
void assertNotContains(string $needle, string $haystack) | Reports an error if $needle is a substring of $haystack. |
void assertNotContains(string $needle, string $haystack, string $message) | Reports an error identified by $message if $needle is a substring of $haystack. |
void assertAttributeContains(mixed $needle, string $haystackAttributeName, string $haystackClassName) | Reports an error if $needle is not an element of $haystackClassName::$haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackClassName::$haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeContains(mixed $needle, string $haystackAttributeName, string $haystackClassName, string $message) | Reports an error identified by $message if $needle is not an element of $haystackClassName::$haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackClassName::$haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeNotContains(mixed $needle, string $haystackAttributeName, string $haystackClassName) | Reports an error if $needle is an element of $haystackClassName::$haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackClassName::$haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeNotContains(mixed $needle, string $haystackAttributeName, string $haystackClassName, string $message) | Reports an error identified by $message if $needle is an element of $haystackClassName::$haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackClassName::$haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeContains(mixed $needle, string $haystackAttributeName, object $haystackObject) | Reports an error if $needle is not an element of $haystackObject->haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackObject->haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeContains(mixed $needle, string $haystackAttributeName, object $haystackObject, string $message) | Reports an error identified by $message if $needle is not an element of $haystackObject->haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackObject->haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeNotContains(mixed $needle, string $haystackAttributeName, object $haystackObject) | Reports an error if $needle is an element of $haystackObject->haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackObject->haystackAttributeName attribute may be public, protected, or private. |
void assertAttributeNotContains(mixed $needle, string $haystackAttributeName, object $haystackObject, string $message) | Reports an error identified by $message if $needle is an element of $haystackObject->haystackAttributeName which can be an array, a string, or an object that implements the Iterator interface. The visibility of the $haystackObject->haystackAttributeName attribute may be public, protected, or private. |
void assertRegExp(string $pattern, string $string) | Reports an error if $string does not match the regular expression $pattern. |
void assertRegExp(string $pattern, string $string, string $message) | Reports an error identified by $message if $string does not match the regular expression $pattern. |
void assertNotRegExp(string $pattern, string $string) | Reports an error if $string matches the regular expression $pattern. |
void assertNotRegExp(string $pattern, string $string, string $message) | Reports an error identified by $message if $string matches the regular expression $pattern. |
void assertType(string $expected, mixed $actual) | Reports an error if the variable $actual is not of type $expected. |
void assertType(string $expected, mixed $actual, string $message) | Reports an error identified by $message if the variable $actual is not of type $expected. |
void assertNotType(string $expected, mixed $actual) | Reports an error if the variable $actual is of type $expected. |
void assertNotType(string $expected, mixed $actual, string $message) | Reports an error identified by $message if the variable $actual is of type $expected. |
void assertFileExists(string $filename) | Reports an error if the file specified by $filename does not exist. |
void assertFileExists(string $filename, string $message) | Reports an error identified by $message if the file specified by $filename does not exist. |
void assertFileNotExists(string $filename) | Reports an error if the file specified by $filename exists. |
void assertFileNotExists(string $filename, string $message) | Reports an error identified by $message if the file specified by $filename exists. |
void assertClassHasAttribute(string $attributeName, string $className) | Reports an error if $className::attributeName does not exist. |
void assertClassHasAttribute(string $attributeName, string $className, string $message) | Reports an error identified by $message if $className::attributeName does not exist. |
void assertClassNotHasAttribute(string $attributeName, string $className) | Reports an error if $className::attributeName exists. |
void assertClassNotHasAttribute(string $attributeName, string $className, string $message) | Reports an error identified by $message if $className::attributeName exists. |
void assertClassHasStaticAttribute(string $attributeName, string $className) | Reports an error if $className::attributeName does not exist or is not static. |
void assertClassHasStaticAttribute(string $attributeName, string $className, string $message) | Reports an error identified by $message if $className::attributeName does not exist or is not static. |
void assertClassNotHasStaticAttribute(string $attributeName, string $className) | Reports an error if $className::attributeName exists and is static. |
void assertClassNotHasStaticAttribute(string $attributeName, string $className, string $message) | Reports an error identified by $message if $className::attributeName exists and is static. |
void assertObjectHasAttribute(string $attributeName, object $object) | Reports an error if $object->attributeName does not exist. |
void assertObjectHasAttribute(string $attributeName, object $object, string $message) | Reports an error identified by $message if $object->attributeName does not exist. |
void assertObjectNotHasAttribute(string $attributeName, object $object) | Reports an error if $object->attributeName exists. |
void assertObjectNotHasAttribute(string $attributeName, object $object, string $message) | Reports an error identified by $message if $object->attributeName exists. |
More complex assertions can be formulated using the
PHPUnit_Framework_Constraint classes. They can be
evaluated using the assertThat() method as shown
in Example 20.3.
Example 20.3: Using assertThat() with constraint objects
<?php
require_once 'PHPUnit/Framework.php';
class ConstraintTest extends PHPUnit_Framework_TestCase
{
public function testNotEquals()
{
$constraint = $this->logicalNot(
$this->equalTo('foo')
);
$this->assertThat('foo', $constraint);
}
}
?>
phpunit ConstraintTest
PHPUnit 3.1.9 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testNotEquals(ConstraintTest)
Failed asserting that <string:foo> is not equal to <string:foo>.
/home/sb/ConstraintTest.php:12
FAILURES!
Tests: 1, Failures: 1.
Table 20.2 shows the available
PHPUnit_Framework_Constraint implementations.
Table 20.2. Constraints
| Constraint | Meaning |
|---|---|
PHPUnit_Framework_Constraint_Attribute attribute(PHPUnit_Framework_Constraint $constraint, $attributeName) | Constraint that applies another constraint to an attribute of a class or an object. |
PHPUnit_Framework_Constraint_IsAnything anything() | Constraint that accepts any input value. |
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $key) | Constraint that asserts that the array it is evaluated for has a given key. |
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $value) | Constraint that asserts that the array or object that implements the Iterator interface it is evaluated for contains a given value. |
PHPUnit_Framework_Constraint_IsEqual equalTo($value, $delta = 0, $maxDepth = 10) | Constraint that checks if one value is equal to another. |
PHPUnit_Framework_Constraint_Attribute attributeEqualTo($attributeName, $value, $delta = 0, $maxDepth = 10) | Constraint that checks if a value is equal to an attribute of a class or of an object. |
PHPUnit_Framework_Constraint_FileExists fileExists() | Constraint that checks if the file(name) that it is evaluated for exists. |
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $value) | Constraint that asserts that the value it is evaluated for is greater than a given value. |
PHPUnit_Framework_Constraint_Or greaterThanOrEqual(mixed $value) | Constraint that asserts that the value it is evaluated for is greater than or equal to a given value. |
PHPUnit_Framework_Constraint_ClassHasAttribute classHasAttribute(string $attributeName) | Constraint that asserts that the class it is evaluated for has a given attribute. |
PHPUnit_Framework_Constraint_ClassHasStaticAttribute classHasStaticAttribute(string $attributeName) | Constraint that asserts that the class it is evaluated for has a given static attribute. |
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $attributeName) | Constraint that asserts that the object it is evaluated for has a given attribute. |
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $value) | Constraint that asserts that one value is identical to another. |
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $className) | Constraint that asserts that the object it is evaluated for is an instance of a given class. |
PHPUnit_Framework_Constraint_IsType isType(string $type) | Constraint that asserts that the value it is evaluated for is of a specified type. |
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $value) | Constraint that asserts that the value it is evaluated for is smaller than a given value. |
PHPUnit_Framework_Constraint_Or lessThanOrEqual(mixed $value) | Constraint that asserts that the value it is evaluated for is smaller than or equal to a given value. |
logicalAnd() | Logical AND. |
logicalNot(PHPUnit_Framework_Constraint $constraint) | Logical NOT. |
logicalOr() | Logical OR. |
logicalXor() | Logical XOR. |
PHPUnit_Framework_Constraint_PCREMatch matchesRegularExpression(string $pattern) | Constraint that asserts that the string it is evaluated for matches a regular expression. |
PHPUnit_Framework_Constraint_StringContains stringContains(string $string, bool $case) | Constraint that asserts that the string it is evaluated for contains a given string. |
You may find that you need other assertions than these to compare
objects specific to your project. Create your own Assert
class to contain these assertions to simplify your tests.
Failing assertions all call a single bottleneck method,
fail(string $message), which throws an
PHPUnit_Framework_AssertionFailedError. There is
also a variant which takes no parameters. Call fail()
explicitly when your test encounters an error. The test for an expected
exception is an example.
Table 20.3 lists the
bottlenext methods in PHPUnit.
Table 20.3. Bottleneck Methods
| Method | Meaning |
|---|---|
void fail() | Reports an error. |
void fail(string $message) | Reports an error identified by $message. |
markTestIncomplete() and markTestSkipped()
are convenience methods for marking a test as being incomplete or skipped.
Table 20.4. Marking a test as being incomplete or skipped
| Method | Meaning |
|---|---|
void markTestIncomplete(string $message) | Marks the current test as being incomplete, $message is optional. |
void markTestSkipped(string $message) | Marks the current test as being skipped, $message is optional. |
Although unit tests are about testing the public interface of a class, you
may sometimes want to test the values of non-public attributes. The
readAttribute() method enables you to do this and
returns the value of a given (static) attribute from a given class or
object.
Table 20.5. Accessing non-public attributes
| Method | Meaning |
|---|---|
Mixed readAttribute($classOrObject, $attributeName) | Returns the value of a given (static) attribute ($attributeName) of a class or of an object. This also works for attributes that are declared protected or private. |
PHPUnit_Framework_Test is the generic interface
used by all objects that can act as tests. Implementors may represent
one or more tests. The two methods are shown in
Table 20.6.
Table 20.6. Implementor Methods
| Method | Meaning |
|---|---|
int count() | Return the number of tests. |
void run(PHPUnit_Framework_TestResult $result) | Run the tests and report the results on $result. |
PHPUnit_Framework_TestCase and
PHPUnit_Framework_TestSuite are the two most
prominent implementors of PHPUnit_Framework_Test.
You can implement PHPUnit_Framework_Test yourself.
The interface is kept small intentionally so it will be easy to
implement.
Your test-case classes will inherit from
PHPUnit_Framework_TestCase. Most of the time, you
will run tests from automatically created test suites. In this case,
each of your tests should be represented by a method named
test* (by convention).
PHPUnit_Framework_TestCase implements
PHPUnit_Framework_Test::count() so that it
always returns 1. The implementation of
PHPUnit_Framework_Test::run(PHPUnit_Framework_TestResult $result)
in this class runs setUp(), runs the test method,
and then runs tearDown(), reporting any exceptions
to the PHPUnit_Framework_TestResult.
Table 20.7 shows the
methods provided by PHPUnit_Framework_TestCase.
Table 20.7. TestCase
| Method | Meaning |
|---|---|
__construct() | Creates a test case. |
__construct(string $name) | Creates a named test case. Names are used to print the test case and often as the name of the test method to be run by reflection. |
string getName() | Return the name of the test case. |
void setName($name) | Set the name of the test case. |
PHPUnit_Framework_TestResult run(PHPUnit_Framework_TestResult $result) | Convenience method to run the test case and report it in $result. |
void runTest() | Override with a testing method if you do not want the testing method to be invoked by reflection. |
object getMock($className, [array $methods, [array $arguments, [string $mockClassName, [boolean $callOriginalConstructor, [boolean $callOriginalClone, [boolean $callAutoload]]]]]]) | Returns a mock object (see Chapter 10) for the specified $className. By default, all methods of the given class are mocked. When the second (optional) parameter is provided, only the methods whose names are in the array are mocked. The third (optional) parameter may hold a parameter array that is passed to the mock object's constructor. The fourth (optional) parameter can be used to specify a class name for the mock object. The fifth (optional) parameter can be used to disable the call to the original object's __construct() method. The sixth (optional) parameter can be used to disable the call to the original object's __clone() method. The seventh (optional) parameter can be used to disable __autoload() during mock object creation. |
void iniSet(string $varName, mixed $newValue) | This method is a wrapper for the ini_set() function that automatically resets the modified php.ini setting to its original value after the test is run. |
void setLocale(integer $category, string $locale, ...) | This method is a wrapper for the setlocale() function that automatically resets the locale to its original value after the test is run. |
There are two template methods -- setUp() and
tearDown() -- you can override to create and
dispose of the objects against which you are going to test.
Table 20.8 shows
these methods. A third template method,
sharedAssertions(), allows to define assertions
that should be performed by all tests of a test case.
Table 20.8. Template Methods
| Method | Meaning |
|---|---|
void setUp() | Override to create objects against which to test. Each test that runs will be run in its own test case, and setUp() will be called separately for each one. |
void sharedAssertions() | Override to perform assertions that are shared by all tests of a test case. This method is called before the execution of a test ends and before tearDown() is called. |
void tearDown() | Override to dispose of objects no longer needed once the test has finished. In general, you only need to explicitly dispose of external resources (files or sockets, for example) in tearDown(). |
A PHPUnit_Framework_TestSuite is a composite of
PHPUnit_Framework_Tests. At its simplest, it
contains a bunch of test cases, all of which are run when the suite is
run. Since it is a composite, however, a suite can contain suites which
can contain suites and so on, making it easy to combine tests from
various sources and run them together.
In addition to the PHPUnit_Framework_Test methods --
run(PHPUnit_Framework_TestResult $result) and
count() -- PHPUnit_Framework_TestSuite
provides methods to create named or unnamed instances.
Table 20.9 shows the
methods to create PHPUnit_Framework_TestSuite
instances.
Table 20.9. Creating named or unnamed instances
| Method | Meaning |
|---|---|
__construct() | Return an empty test suite. |
__construct(string $theClass) | Return a test suite containing an instance of the class named $theClass for each method in the class named test*. If no class of name $theClass exists an empty test suite named $theClass is returned. |
__construct(string $theClass, string $name) | Return a test suite named $name containing an instance of the class named $theClass for each method in the class named test*. |
__construct(ReflectionClass $theClass) | Return a test suite containing an instance of the class represented by $theClass for each method in the class named test*. |
__construct(ReflectionClass $theClass, $name) | Return a test suite named $name containing an instance of the class represented by $theClass for each method in the class named test*. |
string getName() | Return the name of the test suite. |
void setName(string $name) | Set the name of the test suite. |
void markTestSuiteSkipped(string $message) | Marks the current test suite as being skipped, $message is optional. |
PHPUnit_Framework_TestSuite also provides methods
for adding and retrieving PHPUnit_Framework_Tests,
as shown in Table 20.10.
Table 20.10. Adding and retrieving tests
| Method | Meaning |
|---|---|
void addTestSuite(PHPUnit_Framework_TestSuite $suite) | Add another test suite to the test suite. |
void addTestSuite(string $theClass) | Add a test suite containing an instance of the class named $theClass for each method in the class named test* to the test suite. |
void addTestSuite(ReflectionClass $theClass) | Add a test suite containing an instance of the class represented by $theClass for each method in the class named test* to the test suite. |
void addTest(PHPUnit_Framework_Test $test) | Add $test to the suite. |
void addTestFile(string $filename) | Add the tests that are defined in the class(es) of a given sourcefile to the suite. |
void addTestFiles(array $filenames) | Add the tests that are defined in the classes of the given sourcefiles to the suite. |
int testCount() | Return the number of tests directly (not recursively) in this suite. |
PHPUnit_Framework_Test[] tests() | Return the tests directly in this suite. |
PHPUnit_Framework_Test testAt(int $index) | Return the test at the $index. |
Example 20.4 shows how to create and run a test suite.
Example 20.4: Creating and running a test suite
<?php
require_once 'PHPUnit/Framework.php';
require_once 'ArrayTest.php';
// Create a test suite that contains the tests
// from the ArrayTest class.
$suite = new PHPUnit_Framework_TestSuite('ArrayTest');
// Run the tests.
$suite->run();
?>
Chapter 7 shows how to use the
PHPUnit_Framework_TestSuite class to
organize test suites by hierarchically composing test cases.
The PHPUnit_Framework_TestSuite class provides two
template methods -- setUp() and
tearDown() -- that are called before and after the
tests of a test suite are run, respectively.
Table 20.11. Template Methods
| Method | Meaning |
|---|---|
void setUp() | Called before the first test of the test suite is run. |
void tearDown() | Called after the last test of the test suite has been run. |
While you are running all these tests, you need somewhere to store all
the results: how many tests ran, which failed, and how long they took.
PHPUnit_Framework_TestResult collects these results.
A single PHPUnit_Framework_TestResult is passed
around the whole tree of tests; when a test runs or fails, the fact is
noted in the PHPUnit_Framework_TestResult. At the
end of the run, PHPUnit_Framework_TestResult
contains a summary of all the tests.
PHPUnit_Framework_TestResult is also a subject than
can be observed by other objects wanting to report test progress. For
example, a graphical test runner might observe the
PHPUnit_Framework_TestResult and update a progress
bar every time a test starts.
Table 20.12 summarizes
the API of PHPUnit_Framework_TestResult.
Table 20.12. TestResult
| Method | Meaning |
|---|---|
void addError(PHPUnit_Framework_Test $test, Exception $e) | Record that running $test caused $e to be thrown unexpectedly. |
void addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e) | Record that running $test caused $e to be thrown unexpectedly. |
PHPUnit_Framework_TestFailure[] errors() | Return the errors recorded. |
PHPUnit_Framework_TestFailure[] failures() | Return the failures recorded. |
PHPUnit_Framework_TestFailure[] notImplemented() | Return the incomplete test cases recorded. |
int errorCount() | Return the number of errors. |
int failureCount() | Return the number of failures. |
int notImplementedCount() | Return the number of incomplete test cases. |
int count() | Return the total number of test cases run. |
boolean wasSuccessful() | Return whether or not all tests ran successfully. |
boolean allCompletlyImplemented() | Return whether or not all tests were completely implemented. |
void collectCodeCoverageInformation(bool $flag) | Enables or disables the collection of Code Coverage information. |
array getCodeCoverageInformation() | Return the code coverage information collected. |
If you want to register as an observer of a
PHPUnit_Framework_TestResult, you need to implement
PHPUnit_Framework_TestListener. To register, call
addListener(), as shown in
Table 20.13.
Table 20.13. TestResult and TestListener
| Method | Meaning |
|---|---|
void addListener(PHPUnit_Framework_TestListener $listener) | Register $listener to receive updates as results are recorded in the test result. |
void removeListener(PHPUnit_Framework_TestListener $listener) | Unregister $listener from receiving updates. |
Table 20.14 shows the methods that test listeners implement; also see Example 21.3.
Table 20.14. TestListener Callbacks
| Method | Meaning |
|---|---|
void addError(PHPUnit_Framework_Test $test, Exception $e) | $test has thrown $e. |
void addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e) | $test has failed an assertion, throwing a kind of PHPUnit_Framework_AssertionFailedError. |
void addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e) | $test is an incomplete test. |
void addSkippedTest(PHPUnit_Framework_Test $test, Exception $e) | $test is a test that has been skipped. |
void startTestSuite(PHPUnit_Framework_TestSuite $suite) | $suite is about to be run. |
void endTestSuite(PHPUnit_Framework_TestSuite $suite) | $suite has finished running. |
void startTest(PHPUnit_Framework_Test $test) | $test is about to be run. |
void endTest(PHPUnit_Framework_Test $test) | $test has finished running. |
Many of the classes mentioned so far in this book come from
PHPUnit/Framework. Here are all the packages in
PHPUnit:
PHPUnit/Framework
The basic classes in PHPUnit.
PHPUnit/Extensions
Extensions to the PHPUnit framework.
PHPUnit/Runner
Abstract support for running tests.
PHPUnit/TextUI
The text-based test runner.
PHPUnit/Util
Utility classes used by the other packages.
PHPUnit can be extended in various ways to make the writing of tests easier and customize the feedback you get from running tests. Here are common starting points to extend PHPUnit.
Write utility methods in an abstract subclass of
PHPUnit_Framework_TestCase and derive your test case
classes from that class. This is one of the easiest ways to extend
PHPUnit.
You can wrap test cases or test suites in a subclass of
PHPUnit_Extensions_TestDecorator and use the
Decorator design pattern to perform some actions before and after the
test runs.
PHPUnit ships with two concrete test decorators:
PHPUnit_Extensions_RepeatedTest and
PHPUnit_Extensions_TestSetup. The former is used to
run a test repeatedly and only count it as a success if all iterations
are successful. The latter was discussed in Chapter 6.
Example 21.1
shows a cut-down version of the PHPUnit_Extensions_RepeatedTest
test decorator that illustrates how to write your own test decorators.
Example 21.1: The RepeatedTest Decorator
<?php
require_once 'PHPUnit/Extensions/TestDecorator.php';
class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
private $timesRepeat = 1;
public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
{
parent::__construct($test);
if (is_integer($timesRepeat) &&
$timesRepeat >= 0) {
$this->timesRepeat = $timesRepeat;
}
}
public function count()
{
return $this->timesRepeat * $this->test->count();
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = $this->createResult();
}
for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
$this->test->run($result);
}
return $result;
}
}
?>
The PHPUnit_Framework_Test interface is narrow and
easy to implement. You can write an implementation of
PHPUnit_Framework_Test that is simpler than
PHPUnit_Framework_TestCase and that runs
data-driven tests, for instance.
Example 21.2
shows a data-driven test-case class that compares values from a file
with Comma-Separated Values (CSV). Each line of such a file looks like
foo;bar, where the first value is the one we expect
and the second value is the actual one.
Example 21.2: A data-driven test
<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Util/Timer.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
class DataDrivenTest implements PHPUnit_Framework_Test
{
private $lines;
public function __construct($dataFile)
{
$this->lines = file($dataFile);
}
public function count()
{
return 1;
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = new PHPUnit_Framework_TestResult;
}
foreach ($this->lines as $line) {
$result->startTest($this);
PHPUnit_Util_Timer::start();
list($expected, $actual) = explode(';', $line);
try {
PHPUnit_Framework_Assert::assertEquals(trim($expected), trim($actual));
}
catch (PHPUnit_Framework_AssertionFailedError $e) {
$result->addFailure($this, $e, PHPUnit_Util_Timer::stop());
}
catch (Exception $e) {
$result->addError($this, $e, PHPUnit_Util_Timer::stop());
}
$result->endTest($this, PHPUnit_Util_Timer::stop());
}
return $result;
}
}
$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 3.1.9 by Sebastian Bergmann. .F Time: 0 seconds There was 1 failure: 1) DataDrivenTest Failed asserting that two strings are equal. expected string <bar> difference < x> got string <baz> /home/sb/DataDrivenTest.php:32 /home/sb/DataDrivenTest.php:53 FAILURES! Tests: 2, Failures: 1.
By passing a special-purpose PHPUnit_Framework_TestResult
object to the run() method, you can change the way
tests are run and what result data gets collected.
You do not necessarily need to write a whole subclass of
PHPUnit_Framework_TestResult in order to customize
it. Most of the time, it will suffice to implement a new
PHPUnit_Framework_TestListener
(see Table 20.14) and attach
it to the PHPUnit_Framework_TestResult object, before
running the tests.
Example 21.3
shows a simple implementation of the PHPUnit_Framework_TestListener
interface.
Example 21.3: A simple test listener
<?php
require_once 'PHPUnit/Framework.php';
class SimpleTestListener
implements PHPUnit_Framework_TestListener
{
public function
addError(PHPUnit_Framework_Test $test,
Exception $e,
$time)
{
printf(
"Error while running test '%s'.\n",
$test->getName()
);
}
public function
addFailure(PHPUnit_Framework_Test $test,
PHPUnit_Framework_AssertionFailedError $e,
$time)
{
printf(
"Test '%s' failed.\n",
$test->getName()
);
}
public function
addIncompleteTest(PHPUnit_Framework_Test $test,
Exception $e,
$time)
{
printf(
"Test '%s' is incomplete.\n",
$test->getName()
);
}
public function
addSkippedTest(PHPUnit_Framework_Test $test,
Exception $e,
$time)
{
printf(
"Test '%s' has been skipped.\n",
$test->getName()
);
}
public function startTest(PHPUnit_Framework_Test $test)
{
printf(
"Test '%s' started.\n",
$test->getName()
);
}
public function endTest(PHPUnit_Framework_Test $test, $time)
{
printf(
"Test '%s' ended.\n",
$test->getName()
);
}
public function
startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
printf(
"TestSuite '%s' started.\n",
$suite->getName()
);
}
public function
endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
printf(
"TestSuite '%s' ended.\n",
$suite->getName()
);
}
}
?>
Example 21.4 shows how to run and observe a test suite.
Example 21.4: Running and observing a test suite
<?php
require_once 'PHPUnit/Framework.php';
require_once 'ArrayTest.php';
require_once 'SimpleTestListener.php';
// Create a test suite that contains the tests
// from the ArrayTest class.
$suite = new PHPUnit_Framework_TestSuite('ArrayTest');
// Create a test result and attach a SimpleTestListener
// object as an observer to it.
$result = new PHPUnit_Framework_TestResult;
$result->addListener(new SimpleTestListener);
// Run the tests.
$suite->run($result);
?>
TestSuite 'ArrayTest' started. Test 'testNewArrayIsEmpty' started. Test 'testNewArrayIsEmpty' ended. Test 'testArrayContainsAnElement' started. Test 'testArrayContainsAnElement' ended. TestSuite 'ArrayTest' ended.
There is a release series of PHPUnit that works with PHP 4 and does not require PHP 5. Due to PHP 4's limited object model, PHPUnit for PHP 4 is not a complete port of JUnit as PHPUnit for PHP 5 is. It also lacks certain features of PHPUnit for PHP 5, such as code-coverage analysis.
The following command line shows how to install PHPUnit for PHP 4 using the PEAR Installer:
pear install -f http://pear.phpunit.de/get/PHPUnit-1.3.3.tgz
A test-case class that is used with PHPUnit for PHP 4 is similar
to one that is used with PHPUnit for PHP 5. The essential difference is
that such a class extends PHPUnit_TestCase
(which itself extends PHPUnit_Assert, the class that
provides the assertion methods).
Example A.1
shows a version of the ArrayTest test case that can
be used with PHPUnit for PHP 4.
Example A.1: Writing a test case for PHPUnit 1.x
<?php
require_once 'PHPUnit/TestCase.php';
class ArrayTest extends PHPUnit_TestCase
{
var $_fixture;
function setUp()
{
$this->_fixture = array();
}
function testNewArrayIsEmpty()
{
$this->assertEquals(0, sizeof($this->_fixture));
}
function testArrayContainsAnElement()
{
$this->_fixture[] = 'Element';
$this->assertEquals(1, sizeof($this->_fixture));
}
}
?>
PHPUnit for PHP 4 does not provide a TextUI test runner. The most commonly used way to run tests with PHPUnit for PHP 4 is to write a test suite and run it manually, as shown in Example A.2.
Example A.2: Running a test case with PHPUnit 1.x
<?php
require_once 'ArrayTest.php';
require_once 'PHPUnit.php';
$suite = new PHPUnit_TestSuite('ArrayTest');
$result = PHPUnit::run($suite);
print $result->toString();
?>
TestCase arraytest->testnewarrayisempty() passed TestCase arraytest->testarraycontainsanelement() passed
Figure A.1 shows the one feature that PHPUnit for PHP 4 has that PHPUnit for PHP 5 does not yet have: a test runner with a graphical user interface based on PHP-GTK.
Copyright (c) 2005-2009 Sebastian Bergmann.
This work is licensed under the Creative Commons Attribution 3.0
Unported License.
A summary of the license is given below, followed by the full legal
text.
--------------------------------------------------------------------
You are free:
* to Share - to copy, distribute and transmit the work
* to Remix - to adapt the work
Under the following conditions:
Attribution. You must attribute the work in the manner specified by
the author or licensor (but not in any way that suggests that they
endorse you or your use of the work).
* For any reuse or distribution, you must make clear to others
the license terms of this work. The best way to do this is with
a link to this web page.
* Any of the above conditions can be waived if you get
permission from the copyright holder.
* Nothing in this license impairs or restricts the author's moral
rights.
Your fair dealing and other rights are in no way affected by the
above.
This is a human-readable summary of the Legal Code (the full
license) below.
====================================================================
Creative Commons Legal Code
Attribution 3.0 Unported
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO
WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS
LIABILITY FOR DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW
IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
SUCH TERMS AND CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the
Work and other pre-existing works, such as a translation,
adaptation, derivative work, arrangement of music or other
alterations of a literary or artistic work, or phonogram or
performance and includes cinematographic adaptations or any
other form in which the Work may be recast, transformed, or
adapted including in any form recognizably derived from the
original, except that a work that constitutes a Collection
will not be considered an Adaptation for the purpose of this
License. For the avoidance of doubt, where the Work is a
musical work, performance or phonogram, the synchronization of
the Work in timed-relation with a moving image ("synching")
will be considered an Adaptation for the purpose of this
License.
b. "Collection" means a collection of literary or artistic works,
such as encyclopedias and anthologies, or performances,
phonograms or broadcasts, or other works or subject matter
other than works listed in Section 1(f) below, which, by
reason of the selection and arrangement of their contents,
constitute intellectual creations, in which the Work is
included in its entirety in unmodified form along with one or
more other contributions, each constituting separate and
independent works in themselves, which together are assembled
into a collective whole. A work that constitutes a Collection
will not be considered an Adaptation (as defined above) for
the purposes of this License.
c. "Distribute" means to make available to the public the
original and copies of the Work or Adaptation, as appropriate,
through sale or other transfer of ownership.
d. "Licensor" means the individual, individuals, entity or
entities that offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic
work, the individual, individuals, entity or entities who
created the Work or if no individual or entity can be
identified, the publisher; and in addition (i) in the case of
a performance the actors, singers, musicians, dancers, and
other persons who act, sing, deliver, declaim, play in,
interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the
producer being the person or legal entity who first fixes the
sounds of a performance or other sounds; and, (iii) in the
case of broadcasts, the organization that transmits the
broadcast.
f. "Work" means the literary and/or artistic work offered under
the terms of this License including without limitation any
production in the literary, scientific and artistic domain,
whatever may be the mode or form of its expression including
digital form, such as a book, pamphlet and other writing; a
lecture, address, sermon or other work of the same nature; a
dramatic or dramatico-musical work; a choreographic work or
entertainment in dumb show; a musical composition with or
without words; a cinematographic work to which are assimilated
works expressed by a process analogous to cinematography; a
work of drawing, painting, architecture, sculpture, engraving
or lithography; a photographic work to which are assimilated
works expressed by a process analogous to photography; a work
of applied art; an illustration, map, plan, sketch or three-
dimensional work relative to geography, topography,
architecture or science; a performance; a broadcast; a
phonogram; a compilation of data to the extent it is protected
as a copyrightable work; or a work performed by a variety or
circus performer to the extent it is not otherwise considered
a literary or artistic work.
g. "You" means an individual or entity exercising rights under
this License who has not previously violated the terms of
this License with respect to the Work, or who has received
express permission from the Licensor to exercise rights under
this License despite a previous violation.
h. "Publicly Perform" means to perform public recitations of the
Work and to communicate to the public those public
recitations, by any means or process, including by wire or
wireless means or public digital performances; to make
available to the public Works in such a way that members of
the public may access these Works from a place and at a place
individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of
the performances of the Work, including by public digital
performance; to broadcast and rebroadcast the Work by any
means including signs, sounds or images.
i. "Reproduce" means to make copies of the Work by any means
including without limitation by sound or visual recordings and
the right of fixation and reproducing fixations of the Work,
including storage of a protected performance or phonogram in
digital form or other electronic medium.
2. Fair Dealing Rights. Nothing in this License is intended to
reduce, limit, or restrict any uses free from copyright or rights
arising from limitations or exceptions that are provided for in
connection with the copyright protection under copyright law or
other applicable laws.
3. License Grant. Subject to the terms and conditions of this
License, Licensor hereby grants You a worldwide, royalty-free,
non-exclusive, perpetual (for the duration of the applicable
copyright) license to exercise the rights in the Work as stated
below:
a. to Reproduce the Work, to incorporate the Work into one or
more Collections, and to Reproduce the Work as incorporated
in the Collections;
b. to create and Reproduce Adaptations provided that any such
Adaptation, including any translation in any medium, takes
reasonable steps to clearly label, demarcate or otherwise
identify that changes were made to the original Work. For
example, a translation could be marked "The original work was
translated from English to Spanish," or a modification could
indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as
incorporated in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those
jurisdictions in which the right to collect royalties
through any statutory or compulsory licensing scheme cannot
be waived, the Licensor reserves the exclusive right to
collect such royalties for any exercise by You of the
rights granted under this License;
ii. Waivable Compulsory License Schemes. In those
jurisdictions in which the right to collect royalties
through any statutory or compulsory licensing scheme can
be waived, the Licensor waives the exclusive right to
collect such royalties for any exercise by You of the
rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right
to collect royalties, whether individually or, in the
event that the Licensor is a member of a collecting
society that administers voluntary licensing schemes, via
that society, from any exercise by You of the rights
granted under this License.
The above rights may be exercised in all media and formats whether
now known or hereafter devised. The above rights include the right
to make such modifications as are technically necessary to exercise
the rights in other media and formats. Subject to Section 8(f), all
rights not expressly granted by Licensor are hereby reserved.
4. Restrictions. The license granted in Section 3 above is expressly
made subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the
terms of this License. You must include a copy of, or the
Uniform Resource Identifier (URI) for, this License with every
copy of the Work You Distribute or Publicly Perform. You may
not offer or impose any terms on the Work that restrict the
terms of this License or the ability of the recipient of the
Work to exercise the rights granted to that recipient under
the terms of the License. You may not sublicense the Work. You
must keep intact all notices that refer to this License and to
the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or
Publicly Perform the Work, You may not impose any effective
technological measures on the Work that restrict the ability
of a recipient of the Work from You to exercise the rights
granted to that recipient under the terms of the License. This
Section 4(a) applies to the Work as incorporated in a
Collection, but this does not require the Collection apart
from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any
Licensor You must, to the extent practicable, remove from the
Collection any credit as required by Section 4(b), as
requested. If You create an Adaptation, upon notice from any
Licensor You must, to the extent practicable, remove from the
Adaptation any credit as required by Section 4(b), as requested.
b. If You Distribute, or Publicly Perform the Work or any
Adaptations or Collections, You must, unless a request has
been made pursuant to Section 4(a), keep intact all copyright
notices for the Work and provide, reasonable to the medium or
means You are utilizing: (i) the name of the Original Author
(or pseudonym, if applicable) if supplied, and/or if the
Original Author and/or Licensor designate another party or
parties (e.g., a sponsor institute, publishing entity,
journal) for attribution ("Attribution Parties") in Licensor's
copyright notice, terms of service or by other reasonable
means, the name of such party or parties; (ii) the title of
the Work if supplied; (iii) to the extent reasonably
practicable, the URI, if any, that Licensor specifies to be
associated with the Work, unless such URI does not refer to
the copyright notice or licensing information for the Work;
and (iv), consistent with Section 3(b), in the case of an
Adaptation, a credit identifying the use of the Work in the
Adaptation (e.g., "French translation of the Work by Original
Author," or "Screenplay based on original Work by Original
Author"). The credit required by this Section 4 (b) may be
implemented in any reasonable manner; provided, however, that
in the case of a Adaptation or Collection, at a minimum such
credit will appear, if a credit for all contributing authors
of the Adaptation or Collection appears, then as part of these
credits and in a manner at least as prominent as the credits
for the other contributing authors. For the avoidance of
doubt, You may only use the credit required by this Section
for the purpose of attribution in the manner set out above
and, by exercising Your rights under this License, You may not
implicitly or explicitly assert or imply any connection with,
sponsorship or endorsement by the Original Author, Licensor
and/or Attribution Parties, as appropriate, of You or Your use
of the Work, without the separate, express prior written
permission of the Original Author, Licensor and/or
Attribution Parties.
c. Except as otherwise agreed in writing by the Licensor or as
may be otherwise permitted by applicable law, if You
Reproduce, Distribute or Publicly Perform the Work either by
itself or as part of any Adaptations or Collections, You must
not distort, mutilate, modify or take other derogatory action
in relation to the Work which would be prejudicial to the
Original Author's honor or reputation. Licensor agrees that in
those jurisdictions (e.g. Japan), in which any exercise of the
right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion,
mutilation, modification or other derogatory action
prejudicial to the Original Author's honor and reputation, the
Licensor will waive or not assert, as appropriate, this
Section, to the fullest extent permitted by the applicable
national law, to enable You to reasonably exercise Your right
under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF
IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF
THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this
License. Individuals or entities who have received Adaptations
or Collections from You under this License, however, will not
have their licenses terminated provided such individuals or
entities remain in full compliance with those licenses.
Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
this License.
b. Subject to the above terms and conditions, the license granted
here is perpetual (for the duration of the applicable
copyright in the Work). Notwithstanding the above, Licensor
reserves the right to release the Work under different license
terms or to stop distributing the Work at any time; provided,
however that any such election will not serve to withdraw this
License (or any other license that has been, or is required to
be, granted under the terms of this License), and this License
will continue in full force and effect unless terminated as
stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a
Collection, the Licensor offers to the recipient a license to
the Work on the same terms and conditions as the license
granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation,
Licensor offers to the recipient a license to the original
Work on the same terms and conditions as the license granted
to You under this License.
c. If any provision of this License is invalid or unenforceable
under applicable law, it shall not affect the validity or
enforceability of the remainder of the terms of this License,
and without further action by the parties to this agreement,
such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
d. No term or provision of this License shall be deemed waived
and no breach consented to unless such waiver or consent shall
be in writing and signed by the party to be charged with such
waiver or consent.
e. This License constitutes the entire agreement between the
parties with respect to the Work licensed here. There are no
understandings, agreements or representations with respect to
the Work not specified here. Licensor shall not be bound by
any additional provisions that may appear in any communication
from You. This License may not be modified without the mutual
written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced,
in this License were drafted utilizing the terminology of the
Berne Convention for the Protection of Literary and Artistic
Works (as amended on September 28, 1979), the Rome Convention
of 1961, the WIPO Copyright Treaty of 1996, the WIPO
Performances and Phonograms Treaty of 1996 and the Universal
Copyright Convention (as revised on July 24, 1971). These
rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be
enforced according to the corresponding provisions of the
implementation of those treaty provisions in the applicable
national law. If the standard suite of rights granted under
applicable copyright law includes additional rights not
granted under this License, such additional rights are deemed
to be included in the License; this License is not intended to
restrict the license of any rights under applicable law.
Creative Commons is not a party to this License, and makes no
warranty whatsoever in connection with the Work. Creative Commons
will not be liable to You or any party on any legal theory for any
damages whatsoever, including without limitation any general,
special, incidental or consequential damages arising in connection
to this license. Notwithstanding the foregoing two (2) sentences,
if Creative Commons has expressly identified itself as the Licensor
hereunder, it shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of
doubt, this trademark restriction does not form part of this
License.
Creative Commons may be contacted at http://creativecommons.org/.
====================================================================