Copyright © 2005, 2006, 2007, 2008, 2009 Sebastian Bergmann
Edition for PHPUnit 3.4. Updated on 2010-03-09.
assertArrayHasKey()assertClassHasAttribute()assertClassHasStaticAttribute()assertContains()assertContainsOnly()assertEqualXMLStructure()assertEquals()assertFalse()assertFileEquals()assertFileExists()assertGreaterThan()assertGreaterThanOrEqual()assertLessThan()assertLessThanOrEqual()assertNull()assertObjectHasAttribute()assertRegExp()assertSame()assertSelectCount()assertSelectEquals()assertSelectRegExp()assertStringEndsWith()assertStringEqualsFile()assertStringStartsWith()assertTag()assertThat()assertTrue()assertType()assertXmlFileEqualsXmlFile()assertXmlStringEqualsXmlFile()assertXmlStringEqualsXmlString()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 count(). For a newly created
array we expect the count() function to return
0. After we add an element, count()
should return 1.
Example 1.1 shows what
we want to test.
Example 1.1: Testing array operations
<?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 count() before
and after adding the element (see
Example 1.2).
If we get 0 and then 1,
array and count() behave as
expected.
Example 1.2: Using print to test array operations
<?php
$fixture = array();
print count($fixture) . "\n";
$fixture[] = 'element';
print count($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 operations
<?php
$fixture = array();
print count($fixture) == 0 ? "ok\n" : "not ok\n";
$fixture[] = 'element';
print count($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 operations
<?php
$fixture = array();
assertTrue(count($fixture) == 0);
$fixture[] = 'element';
assertTrue(count($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 3.4 requires PHP 5.1.4 (or later) but PHP 5.3.2 (or later) is highly recommended. It 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. Furthermore, a component that PHPUnit depends upon is hosted on
the Symfony Components PEAR channel (pear.symfony-project.com).
pear channel-discover pear.phpunit.depear channel-discover pear.symfony-project.comThis 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 phpunit.php 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).
Prepare the PHPUnit/Util/PHP.php script:
Replace the @php_bin@ string in it with the
path to your PHP command-line interpreter (usually
/usr/bin/php).
Example 4.1 shows how we can write tests using PHPUnit that exercice PHP's array operations. The example introduces the basic conventions and 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 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 the section called “PHPUnit_Framework_Assert”) are used to assert that an actual value matches an expected value.
Example 4.1: Testing array operations with PHPUnit
<?php
require_once 'PHPUnit/Framework.php';
class StackTest extends PHPUnit_Framework_TestCase
{
public function testPushAndPop()
{
$stack = array();
$this->assertEquals(0, count($stack));
array_push($stack, 'foo');
$this->assertEquals('foo', $stack[count($stack)-1]);
$this->assertEquals(1, count($stack));
$this->assertEquals('foo', array_pop($stack));
$this->assertEquals(0, count($stack));
}
}
?>
Whenever you are tempted to type something into a
| ||
| --Martin Fowler | ||
Unit Tests are primarily written as a good practice to help developers identify and fix bugs, to refactor code and to serve as documentation for a unit of software under test. To achieve these benefits, unit tests ideally should cover all the possible paths in a program. One unit test usually covers one specific path in one function or method. However a test method is not necessary an encapsulated, independent entity. Often there are implicit dependencies between test methods, hidden in the implementation scenario of a test. | ||
| --Adrian Kuhn et. al. | ||
PHPUnit supports the declaration of explicit dependencies between test methods. Such dependencies do not define the order in which the test methods are to be executed but they allow the returning of an instance of the test fixture by a producer and passing it to the dependent consumers.
A producer is a test method that yields its unit under test as return value.
A consumer is a test method that depends on one or more producers and their return values.
Example 4.2 shows
how to use the @depends annotation to express
dependencies between test methods.
Example 4.2: Using the @depends annotation to express dependencies
<?php
class StackTest extends PHPUnit_Framework_TestCase
{
public function testEmpty()
{
$stack = array();
$this->assertTrue(empty($stack));
return $stack;
}
/**
* @depends testEmpty
*/
public function testPush(array $stack)
{
array_push($stack, 'foo');
$this->assertEquals('foo', $stack[count($stack)-1]);
$this->assertFalse(empty($stack));
return $stack;
}
/**
* @depends testPush
*/
public function testPop(array $stack)
{
$this->assertEquals('foo', array_pop($stack));
$this->assertTrue(empty($stack));
}
}
?>
In the example above, the first test, testEmpty(),
creates a new array and asserts that it is empty. The test then returns
the fixture as its result. The second test, testPush(),
depends on testEmpty() and is passed the result of that
depended-upon test as its argument. Finally, testPop()
depends upons testPush().
To quickly localize defects, we want our attention to be focussed on relevant failing tests. This is why PHPUnit skips the execution of a test when a depended-upon test has failed. This improves defect localization by exploiting the dependencies between tests as shown in Example 4.3.
Example 4.3: Exploiting the dependencies between tests
<?php
class DependencyFailureTest extends PHPUnit_Framework_TestCase
{
public function testOne()
{
$this->assertTrue(FALSE);
}
/**
* @depends testOne
*/
public function testTwo()
{
}
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 3.4.2 by Sebastian Bergmann.
DependencyFailureTest
FS
Time: 0 seconds
There was 1 failure:
1) testOne(DependencyFailureTest)
Failed asserting that <boolean:false> is true.
/home/sb/DependencyFailureTest.php:6
There was 1 skipped test:
1) testTwo(DependencyFailureTest)
This test depends on "DependencyFailureTest::testOne" to pass.
FAILURES!
Tests: 2, Assertions: 1, Failures: 1, Skipped: 1.
A test may have more than one @depends annotation.
PHPUnit does not change the order in which tests are executed, you have to
ensure that the dependencies of a test can actually be met before the test
is run.
A test method can accept arbitrary arguments. These arguments are to be
provided by a data provider method (provider() in
Example 4.4).
The data provider method to be used is specified using the
@dataProvider annotation.
A data provider method must be public and either return
an array of arrays or an object that implements the Iterator
interface and yields an array for each iteration step. For each array that
is part of the collection the test method will be called with the contents
of the array as its arguments.
Example 4.4: Using data providers
<?php
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider provider
*/
public function testAdd($a, $b, $c)
{
$this->assertEquals($c, $a + $b);
}
public function provider()
{
return array(
array(0, 0, 0),
array(0, 1, 1),
array(1, 0, 1),
array(1, 1, 3)
);
}
}
?>
phpunit DataTest
PHPUnit 3.4.2 by Sebastian Bergmann.
...F
Time: 0 seconds
There was 1 failure:
1) testAdd(DataTest) with data (1, 1, 3)
Failed asserting that <integer:2> matches expected value <integer:3>.
/home/sb/DataTest.php:21
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
Example 4.5
shows how to use the @expectedException annotation to
test whether an exception is thrown inside the tested code.
Example 4.5: Using the @expectedException annotation
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
}
}
?>
phpunit ExceptionTest
PHPUnit 3.4.2 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testException(ExceptionTest)
Expected exception InvalidArgumentException
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
Alternatively, you can use the setExpectedException()
method to set the expected exception as shown in Example 4.6.
Example 4.6: Expecting an exception to be raised by the tested code
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase
{
public function testException()
{
$this->setExpectedException('InvalidArgumentException');
}
}
?>
phpunit ExceptionTest
PHPUnit 3.4.2 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testException(ExceptionTest)
Expected exception InvalidArgumentException
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.Table 4.1 shows the methods provided for testing exceptions.
Table 4.1. Methods for testing exceptions
| Method | Meaning |
|---|---|
void setExpectedException(string $exceptionName) | Set the name of the expected exception to $exceptionName. |
String getExpectedException() | Return the name of the expected exception. |
You can also use the approach shown in Example 4.7 to test exceptions.
Example 4.7: 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 (InvalidArgumentException $expected) {
return;
}
$this->fail('An expected exception has not been raised.');
}
}
?>
If the code that is expected to raise an exception in
Example 4.7
does not raise the expected exception, the subsequent call to
fail() (see
Table 22.2) 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.
By default, PHPUnit converts PHP errors, warnings, and notices that are triggered during the execution of a test to an exception. Using these exceptions, you can, for instance, expect a test to trigger a PHP error as shown in Example 4.8.
Example 4.8: Expecting a PHP error using @expectedException
<?php
class ExpectedErrorTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException PHPUnit_Framework_Error
*/
public function testFailingInclude()
{
include 'not_existing_file.php';
}
}
?>
phpunit ExpectedErrorTest
PHPUnit 3.4.2 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 1 assertion)
PHPUnit_Framework_Error_Notice and
PHPUnit_Framework_Error_Warning represent PHP notices
and warnings, respectively.
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.4.2 by Sebastian Bergmann.
..
Time: 0 seconds
OK (2 tests, 2 assertions)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 10).
IPrinted when the test is marked as being incomplete or not yet implemented (see Chapter 10).
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.4.2 by Sebastian Bergmann.
Usage: phpunit [switches] UnitTest [UnitTest.php]
phpunit [switches] <directory>
--log-junit <file> Log test execution in JUnit XML format to file.
--log-tap <file> Log test execution in TAP format to file.
--log-json <file> Log test execution in JSON format.
--coverage-html <dir> Generate code coverage report in HTML format.
--coverage-clover <file> Write code coverage data in Clover XML format.
--coverage-source <dir> Write code coverage / source data in XML format.
--story-html <file> Write Story/BDD results in HTML format to file.
--story-text <file> Write Story/BDD results in Text format to file.
--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.
--group ... Only runs tests from the specified group(s).
--exclude-group ... Exclude tests from the specified group(s).
--list-groups List available test groups.
--loader <loader> TestSuiteLoader implementation to use.
--story Report test execution progress in Story/BDD format.
--tap Report test execution progress in TAP format.
--testdox Report test execution progress in TestDox format.
--colors Use colors in output.
--stderr Write to STDERR instead of STDOUT.
--stop-on-failure Stop execution upon first error or failure.
--verbose Output more verbose information.
--wait Waits for a keystroke after each test.
--skeleton-class Generate Unit class for UnitTest in UnitTest.php.
--skeleton-test Generate UnitTest class for Unit in Unit.php.
--process-isolation Run each test in a separate PHP process.
--no-globals-backup Do not backup and restore $GLOBALS for each test.
--static-backup Backup and restore static attributes for each test.
--syntax-check Try to check source files for syntax errors.
--bootstrap <file> A "bootstrap" PHP file that is run before the tests.
--configuration <file> Read configuration from XML file.
--no-configuration Ignore default configuration file (phpunit.xml).
--include-path <path(s)> Prepend PHP's include_path with given path(s).
-d key[=value] Sets a php.ini value.
--help Prints this usage information.
--version Prints the version and exits.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-jsonGenerates a logfile using the JSON format. See Chapter 19 for more details.
--log-junitGenerates a logfile in JUnit XML format for the tests run. See Chapter 19 for more details.
--log-tapGenerates a logfile using the Test Anything Protocol (TAP) format for the tests run. See Chapter 19 for more details.
--coverage-htmlGenerates a code coverage report in HTML format. See Chapter 15 for more details.
Please note that this functionality is only available when the tokenizer and Xdebug extensions are installed.
--coverage-cloverGenerates a logfile in XML format with the code coverage information for the tests run. See Chapter 19 for more details.
Please note that this functionality is only available when the tokenizer and Xdebug extensions are installed.
--coverage-sourceGenerates one XML file per covered PHP source file to a given directory. Each <line> element holds a line of PHP sourcecode that is annotated with code coverage information.
Please note that this functionality is only available when the tokenizer and Xdebug extensions are installed.
--story-html and --story-textGenerates reports in HTML or plain text format for the Behaviour-Driven Development scenarios that are run. See Chapter 14 for more details.
--testdox-html and --testdox-textGenerates agile documentation in HTML or plain text format for the tests that are run. See Chapter 16 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.
--group
Only runs tests from the specified group(s). A test can be tagged as
belonging to a group using the @group annotation.
--exclude-group
Exclude tests from the specified group(s). A test can be tagged as
belonging to a group using the @group annotation.
--list-groupsList available test groups.
--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.
--storyReports the test progress in a format that fits Behaviour-Driven Development. See Chapter 14 for more details.
--tapReports the test progress using the Test Anything Protocol (TAP). See Chapter 19 for more details.
--testdoxReports the test progress as agile documentation. See Chapter 16 for more details.
--colorsUse colors in output.
--syntax-checkEnables the syntax check of test source files.
--stderr
Optionally print to STDERR instead of
STDOUT.
--stop-on-failureStop execution upon first error or failure.
--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-class
Generates a skeleton class Unit
(in Unit.php) from a test case class
UnitTest (in UnitTest.php).
See Chapter 17 for more details.
--skeleton-test
Generates a skeleton test case class UnitTest
(in UnitTest.php) for a class
Unit (in Unit.php).
See Chapter 17 for more details.
--process-isolationRun each test in a separate PHP process.
--no-globals-backupDo not backup and restore $GLOBALS. See the section called “Global State” for more details.
--static-backupBackup and restore static attributes of user-defined classes. See the section called “Global State” for more details.
--bootstrapA "bootstrap" PHP file that is run before the tests.
--configurationRead configuration from XML file. See Appendix C for more details.
If phpunit.xml or
phpunit.xml.dist (in that order) exist in the
current working directory and --configuration is
not used, the configuration will be automatically
read from that file.
--no-configuration
Ignore phpunit.xml and
phpunit.xml.dist from the current working
directory.
--include-path
Prepend PHP's include_path with given path(s).
-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.
In Example 4.2 we
used the producer-consumer relationship between tests to share fixture. This
is not always desired or even possible. Example 6.1
shows how we can write the tests of the StackTest in such
a way that not the fixture itself is reused but the code that creates it.
First we declare the instance variable, $stack, 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->stack, instead of the method-local variable
$stack with the assertEquals()
assertion method.
Example 6.1: Using setUp() to create the stack fixture
<?php
class StackTest extends PHPUnit_Framework_TestCase
{
protected $stack;
protected function setUp()
{
$this->stack = array();
}
public function testEmpty()
{
$this->assertTrue(empty($this->stack));
}
public function testPush()
{
array_push($this->stack, 'foo');
$this->assertEquals('foo', $this->stack[count($this->stack)-1]);
$this->assertFalse(empty($this->stack));
}
public function testPop()
{
array_push($this->stack, 'foo');
$this->assertEquals('foo', array_pop($this->stack));
$this->assertTrue(empty($this->stack));
}
}
?>
The setUp() and tearDown() template
methods are run once for each test method (and on fresh instances) of the
test case class.
In addition, the setUpBeforeClass() and
tearDownAfterClass() template methods are called before
the first test of the test case class is run and after the last test of the
test case class is run, respectively.
The example below shows all template methods that are available in a test case class.
Example 6.2: Example showing all template methods available
<?php
require_once 'PHPUnit/Framework.php';
class TemplateMethodsTest extends PHPUnit_Framework_TestCase
{
public static function setUpBeforeClass()
{
print __METHOD__ . "\n";
}
protected function setUp()
{
print __METHOD__ . "\n";
}
protected function assertPreConditions()
{
print __METHOD__ . "\n";
}
public function testOne()
{
print __METHOD__ . "\n";
$this->assertTrue(TRUE);
}
public function testTwo()
{
print __METHOD__ . "\n";
$this->assertTrue(FALSE);
}
protected function assertPostConditions()
{
print __METHOD__ . "\n";
}
protected function tearDown()
{
print __METHOD__ . "\n";
}
public static function tearDownAfterClass()
{
print __METHOD__ . "\n";
}
protected function onNotSuccessfulTest(Exception $e)
{
print __METHOD__ . "\n";
throw $e;
}
}
?>
phpunit TemplateMethodsTest
PHPUnit 3.4.2 by Sebastian Bergmann.
TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass
Time: 0 seconds
There was 1 failure:
1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
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.3
uses the setUp() and tearDown()
template methods of the PHPUnit_Framework_TestSuite
class (see the section called “Using the TestSuite Class”) 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.3: 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 11), than by creating dependencies between tests at runtime and ignoring the opportunity to improve your design.
It is hard to test code that uses singletons. The same is true for code that uses global variables. Typically, the code you want to test is coupled strongly with a global variable and you cannot control its creation. An additional problem is the fact that one test's change to a global variable might break another test.
In PHP, global variables work like this:
A global variable $foo = 'bar'; is stored as $GLOBALS['foo'] = 'bar';.
The $GLOBALS variable is a so-called super-global variable.
Super-global variables are built-in variables that are always available in all scopes.
In the scope of a function or method, you may access the global variable $foo by either directly accessing $GLOBALS['foo'] or by using global $foo; to create a local variable with a reference to the global variable.
Besides global variables, static attributes of classes are also part of the global state.
By default, PHPUnit runs your tests in a way where changes to global
and super-global variables ($GLOBALS,
$_ENV, $_POST,
$_GET, $_COOKIE,
$_SERVER, $_FILES,
$_REQUEST) do not affect other tests. Optionally, this
isolation can be extended to static attributes of classes.
The implementation of the backup and restore operations for static attributes of classes requires PHP 5.3 (or greater).
The implementation of the backup and restore operations for global
variables and static attributes of classes uses serialize()
and unserialize().
Objects of some classes that are provided by PHP itself, such as
PDO for example, cannot be serialized and the backup
operation will break when such an object is stored in the
$GLOBALS array, for instance.
The @backupGlobals annotation that is discussed in
the section called “@backupGlobals” can be used to
control the backup and restore operations for global variables.
Alternatively, you can provide a blacklist of global variables that are to
be excluded from the backup and restore operations like this
class MyTest extends PHPUnit_Framework_TestCase
{
protected $backupGlobalsBlacklist = array('globalVariable');
// ...
}
Please note that setting the $backupGlobalsBlacklist
attribute inside the setUp() method, for instance,
has no effect.
The @backupStaticAttributes annotation that is
discussed in the section called “@backupStaticAttributes”
can be used to control the backup and restore operations for static
attributes. Alternatively, you can provide a blacklist of static
attributes that are to be excluded from the backup and restore operations
like this
class MyTest extends PHPUnit_Framework_TestCase
{
protected $backupStaticAttributesBlacklist = array(
'className' => array('attributeName')
);
// ...
}
Please note that setting the $backupStaticAttributesBlacklist
attribute inside the setUp() method, for instance,
has no effect.
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.
PHPUnit supports different ways of organizing tests and composing them into a test suite. This chapter shows the most commonly used approaches.
Probably the easiest way to compose a test suite is to keep all test case source files in a test directory. PHPUnit can automatically discover and run the tests by recursively traversing the test directory.
Lets take a look at the test suite of the Object_Freezer
library. Looking at this project's directory structure, we see that the
test case classes in the Tests directory mirror the
package and class structure of the System Under Test (SUT) in the
Object directory:
Object Tests |-- Freezer |-- Freezer | |-- HashGenerator | |-- HashGenerator | | `-- NonRecursiveSHA1.php | | `-- NonRecursiveSHA1Test.php | |-- HashGenerator.php | | | |-- IdGenerator | |-- IdGenerator | | `-- UUID.php | | `-- UUIDTest.php | |-- IdGenerator.php | | | |-- LazyProxy.php | | | |-- Storage | |-- Storage | | `-- CouchDB.php | | `-- CouchDB | | | | |-- WithLazyLoadTest.php | | | | `-- WithoutLazyLoadTest.php | |-- Storage.php | |-- StorageTest.php | `-- Util.php | `-- UtilTest.php `-- Freezer.php `-- FreezerTest.php
To run all tests for the library we just need to point the PHPUnit command-line test runner to the test directory:
phpunit Tests
PHPUnit 3.4.2 by Sebastian Bergmann.
............................................................ 60 / 75
...............
Time: 0 seconds
OK (75 tests, 164 assertions)
To run only the tests that are declared in the Object_FreezerTest
test case class in Tests/FreezerTest.php we can use
the following command:
phpunit Tests/FreezerTest
PHPUnit 3.4.2 by Sebastian Bergmann.
............................
Time: 0 seconds
OK (28 tests, 60 assertions)
For more fine-grained control of which tests to run we can use the
--filter switch:
phpunit --filter testFreezingAnObjectWorks Tests
PHPUnit 3.4.2 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 2 assertions)A drawback of this approach is that we have no control over the order in which the test are run. This can lead to problems with regard to test dependencies, see the section called “Test Dependencies”.
PHPUnit's XML configuration file (Appendix C)
can also be used to compose a test suite.
Example 7.1
shows a minimal example that will add all *Test classes
that are found in *Test.php files when the
Tests is recursively traversed.
Example 7.1: Composing a Test Suite Using XML Configuration
<phpunit
<testsuites>
<testsuite name="Object_Freezer">
<directory>Tests</directory>
</testsuite>
</testsuites>
</phpunit>A drawback of this approach is that we have no control over the order in which the test are run. This can lead to problems with regard to test dependencies, see the section called “Test Dependencies”.
Alternatively, we can make the order in which tests are executed explicit:
Example 7.2: Composing a Test Suite Using XML Configuration
<phpunit
<testsuites>
<testsuite name="Object_Freezer">
<file>Tests/Freezer/HashGenerator/NonRecursiveSHA1Test.php</file>
<file>Tests/Freezer/IdGenerator/UUIDTest.php</file>
<file>Tests/Freezer/UtilTest.php</file>
<file>Tests/FreezerTest.php</file>
<file>Tests/Freezer/StorageTest.php</file>
<file>Tests/Freezer/Storage/CouchDB/WithLazyLoadTest.php</file>
<file>Tests/Freezer/Storage/CouchDB/WithoutLazyLoadTest.php</file>
</testsuite>
</testsuites>
</phpunit>
The PHPUnit_Framework_TestSuite class of the PHPUnit
framework allows us to organize tests into a hierarchy of test suite
objects.
Example 7.3 shows
the top-level AllTests class for a project that has a
package named Package.
Example 7.3: The top-level AllTests class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Package/AllTests.php';
// ...
class AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('Project');
$suite->addTest(Package_AllTests::suite());
// ...
return $suite;
}
}
?>
The top-level AllTests class aggregates the
package-level Package_AllTests class that in turn
aggregates the test case classes for the classes of said package.
Example 7.4: The Package_AllTests class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Framework/ClassTest.php';
// ...
class Package_AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('Package');
$suite->addTestSuite('Package_ClassTest');
// ...
return $suite;
}
}
?>
The Package_ClassTest 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 AllTests in the
Tests/Package directory will run only the tests
for the Package_* classes.
Executing phpunit ClassTest in the
Tests/Package directory will run only the tests
for the Package_Class class (which are declared in
the Package_ClassTest class).
Executing phpunit --filter testSomething ClassTest
in the Tests/Package directory will run only the
test named testSomething from the
Package_ClassTest 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.5: 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 __METHOD__ . "\n";
}
protected function tearDown()
{
print __METHOD__ . "\n";
}
}
?>
The MyTest test case class that is added to the test
suite MySuite in
Example 7.5
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”).
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.
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.1
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.1: 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.4.2 by Sebastian Bergmann.
.F
Time: 0 seconds
There was 1 failure:
1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ -1 +1 @@
-bar
+baz
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
Table 8.1
shows the methods provided by
PHPUnit_Extensions_OutputTestCase.
Table 8.1. 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. |
There are two other extensions to PHPUnit_Framework_TestCase,
PHPUnit_Extensions_Database_TestCase and
PHPUnit_Extensions_SeleniumTestCase, that are covered
in Chapter 9 and Chapter 18,
respectively.
While creating tests for your software you may come across database code that needs to be unit tested. The database extension has been created to provide an easy way to place your database in a known state, execute your database-effecting code, and ensure that the expected data is found in the database.
The quickest way to create a new Database Unit Test is to extend the
PHPUnit_Extensions_Database_TestCase class. This class
provides the functionality to create a database connection, seed your
database with data, and after executing a test comparing the contents of
your database with a data set that can be built in a variety of formats. In
Example 9.1 you can see
examples of getConnection() and getDataSet()
implementations.
Example 9.1: Setting up a database test case
<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getConnection()
{
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
return $this->createDefaultDBConnection($pdo, 'testdb');
}
protected function getDataSet()
{
return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');
}
}
?>
The getConnection() method must return an
implementation of the PHPUnit_Extensions_Database_DB_IDatabaseConnection
interface. The createDefaultDBConnection() method can
be used to return a database connection. It accepts a PDO
object as the first parameter and the name of the schema you are testing
against as the second parameter.
The getDataSet() method must return an implementation of
the PHPUnit_Extensions_Database_DataSet_IDataSet
interface. There are currently three different data sets available in
PHPUnit. These data sets are discussed in the section called “Data Sets”
Table 9.1. Database Test Case Methods
| Method | Meaning |
|---|---|
PHPUnit_Extensions_Database_DB_IDatabaseConnection getConnection() | Implement to return the database connection that will be checked for expected data sets and tables. |
PHPUnit_Extensions_Database_DataSet_IDataSet getDataSet() | Implement to return the data set that will be used in in database set up and tear down operations. |
PHPUnit_Extensions_Database_Operation_DatabaseOperation getSetUpOperation() | Override to return a specific operation that should be performed on the test database at the beginning of each test. The various operations are detailed in the section called “Operations”. |
PHPUnit_Extensions_Database_Operation_DatabaseOperation getTearDownOperation() | Override to return a specific operation that should be performed on the test database at the end of each test. The various operations are detailed in the section called “Operations”. |
PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection createDefaultDBConnection(PDO $connection, string $schema) | Return a database connection wrapper around the $connection PDO object. The database schema being tested against should be specified by $schema. The result of this method can be returned from getConnection(). |
PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet createFlatXMLDataSet(string $xmlFile) | Returns a flat XML data set that is created from the XML file located at the absolute path specified in $xmlFile. More details about flat XML files can be found in the section called “Flat XML Data Set”. The result of this method can be returned from getDataSet(). |
PHPUnit_Extensions_Database_DataSet_XmlDataSet createXMLDataSet(string $xmlFile) | Returns a XML data set that is created from the XML file located at the absolute path specified in $xmlFile. More details about XML files can be found in the section called “XML Data Set”. The result of this method can be returned from getDataSet(). |
void assertTablesEqual(PHPUnit_Extensions_Database_DataSet_ITable $expected, PHPUnit_Extensions_Database_DataSet_ITable $actual) | Reports an error if the contents of the $expected table do not match the contents in the $actual table. |
void assertDataSetsEqual(PHPUnit_Extensions_Database_DataSet_IDataSet $expected, PHPUnit_Extensions_Database_DataSet_IDataSet $actual) | Reports an error if the contents of the $expected data set do not match the contents in the $actual data set. |
Data sets are the basic building blocks for both your database fixture as
well as the assertions you may make at the end of your test. When
returning a data set as a fixture from the PHPUnit_Extensions_Database_TestCase::getDataSet()
method, the default implementation in PHPUnit will automatically truncate
all tables specified and then insert the data from your data set in the
order specified by the data set. For your convenience there are several
different types of data sets that can be used at your convenience.
The flat XML data set is a very simple XML format that uses a single XML element for each row in your data set. An example of a flat XML data set is shown in Example 9.2.
Example 9.2: A Flat XML Data Set
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<post
post_id="1"
title="My First Post"
date_created="2008-12-01 12:30:29"
contents="This is my first post" rating="5"
/>
<post
post_id="2"
title="My Second Post"
date_created="2008-12-04 15:35:25"
contents="This is my second post"
/>
<post
post_id="3"
title="My Third Post"
date_created="2008-12-09 03:17:05"
contents="This is my third post"
rating="3"
/>
<post_comment
post_comment_id="2"
post_id="2"
author="Frank"
content="That is because this is simply an example."
url="http://phpun.it/"
/>
<post_comment
post_comment_id="1"
post_id="2"
author="Tom"
content="While the topic seemed interesting the content was lacking."
/>
<current_visitors />
</dataset>
As you can see the formatting is extremely simple. Each of the elements
within the root <dataset> element represents a
row in the test database with the exception of the last
<current_visitors /> element (this will be
discussed shortly.) The name of the element is the equivalent of a
table name in your database. The name of each attribute is the
equivalent of a column name in your databases. The value of each
attribute is the equivalent of the value of that column in that row.
This format, while simple, does have some special considerations. The
first of these is how you deal with empty tables. With the default
operation of CLEAN_INSERT you can specify that you
want to truncate a table and not insert any values by listing that
table as an element without any attributes. This can be seen in
Example 9.2 with the
<current_visitors /> element. The most common
reason you would want to ensure an empty table as a part of your
fixture is when your test should be inserting data to that table. The
less data your database has, the quicker your tests will run. So if I
where testing my simple blogging software's ability to add comments to
a post, I would likely change
Example 9.2 to specify
post_comment as an empty table. When dealing with
assertions it is often useful to ensure that a table is not being
unexpectedly written to, which could also be accomplished in FlatXML
using the empty table format.
The second consideration is how NULL values are
defined. The nature of the flat XML format only allows you to
explicitly specify strings for column values. Of course your database
will convert a string representation of a number or date into the
appropriate data type. However, there are no string representations of
NULL. You can imply a NULL value
by leaving a column out of your element's attribute list. This will
cause a NULL value to be inserted into the database for that column.
This leads me right into my next consideration that makes implicit
NULL values somewhat difficult to deal with.
The third consideration is how columns are defined. The column list for
a given table is determined by the attributes in the first element for
any given table.
In Example 9.2 the
post table would be considered to have the columns
post_id, title,
date_created, contents and
rating. If the first <post>
were removed then the post would no longer be
considered to have the rating column. This means that the first element
of a given name defines the structure of that table. In the simplest of
examples, this means that your first defined row must have a value for
every column that you expect to have values for in the rest of rows for
that table. If an element further into your data set specifies a column
that was not specified in the first element then that value will be
ignored. You can see how this influenced the order of elements in my
dataset in Example 9.2.
I had to specify the second <post_comment>
element first due to the non-NULL value in the url
column.
There is a way to work around the inability to explicitly set
NULL in a flat XML data using the Replacement data
set type. This will be discussed further in
the section called “Replacement Data Set”.
While the flat XML data set is simple it also proves to be limiting. A more powerful xml alternative is the XML data set. It is a more structured xml format that allows you to be much more explicit with your data set. An example of the XML data set equivalent to the previous flat XML example is shown in Example 9.3.
Example 9.3: A XML Data Set
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<table name="post">
<column>post_id</column>
<column>title</column>
<column>date_created</column>
<column>contents</column>
<column>rating</column>
<row>
<value>1</value>
<value>My First Post</value>
<value>2008-12-01 12:30:29</value>
<value>This is my first post</value>
<value>5</value>
</row>
<row>
<value>2</value>
<value>My Second Post</value>
<value>2008-12-04 15:35:25</value>
<value>This is my second post</value>
<null />
</row>
<row>
<value>3</value>
<value>My Third Post</value>
<value>2008-12-09 03:17:05</value>
<value>This is my third post</value>
<value>3</value>
</row>
</table>
<table name="post_comment">
<column>post_comment_id</column>
<column>post_id</column>
<column>author</column>
<column>content</column>
<column>url</column>
<row>
<value>1</value>
<value>2</value>
<value>Tom</value>
<value>While the topic seemed interesting the content was lacking.</value>
<null />
</row>
<row>
<value>2</value>
<value>2</value>
<value>Frank</value>
<value>That is because this is simply an example.</value>
<value>http://phpun.it</value>
</row>
</table>
<table name="current_visitors">
<column>current_visitors_id</column>
<column>ip</column>
</table>
</dataset>
The formatting is more verbose than that of the Flat XML data set but
in some ways much easier to understand. The root element is again the
<dataset> element. Then directly under that
element you will have one or more <table>
elements. Each <table> element must have a
name attribute with a value equivalent to the name
of the table being represented. The <table>
element will then include one or more <column>
elements containing the name of a column in that table. The
<table> element will also include any number
(including zero) of <row> elements. The
<row> element will be what ultimately
specifies the data to store/remove/update in the database or to check
the current database against. The <row>
element must have the same number of children as there are
<column> elements in that
<table> element. The order of your child
elements is also determined by the order of your
<column> elements for that table. There are
two different elements that can be used as children of the
<row> element. You may use the
<value> element to specify the value of that
column in much the same way you can use attributes in the flat XML data
set. The content of the <value> element will
be considered the content of that column for that row. You may also use
the <null> element to explicitly indicate
that the column is to be given a NULL value. The
<null> element does not contain any attributes
or children.
You can use the DTD in Example 9.4 to validate your XML data set files. A reference of the valid elements in an XML data set can be found in Table 9.2
Example 9.4: The XML Data Set DTD
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT dataset (table+) | ANY>
<!ELEMENT table (column*, row*)>
<!ATTLIST table
name CDATA #REQUIRED
>
<!ELEMENT column (#PCDATA)>
<!ELEMENT row (value | null | none)*>
<!ELEMENT value (#PCDATA)>
<!ELEMENT null EMPTY>
Table 9.2. XML Data Set Element Description
| Element | Purpose | Contents | Attributes |
|---|---|---|---|
<dataset> | The root element of the xml data set file. | One or more <table> elements. | None |
<table> | Defines a table in the dataset. | One or more <column> elements and zero or more <row> elements. | name - The name of the table. |
<column> | Defines a column in the current table. | A text node containing the name of the column. | None |
<row> | Defines a row in the table. | One or more <value> or <null> elements. | None |
<value> | Sets the value of the column in the same position as this value. | A text node containing the value of the corresponding column. | None |
<null> | Sets the value of the column in the same position of this value to NULL. | None | None |
While XML data sets provide a convenient way to structure your data, in many cases they can be time consuming to hand edit as there are not very many XML editors that provide an easy way to edit tabular data via xml. In these cases you may find the CSV data set to be much more useful. The CSV data set is very simple to understand. Each CSV file represents a table. The first row in the CSV file must contain the column names and all subsequent rows will contain the data for those columns.
To construct a CSV data set you must instantiate the PHPUnit_Extensions_Database_DataSet_CsvDataSet class. The constructor takes three parameters: $delimiter, $enclosure and $escape. These parameters allow you to specify the exact formatting of rows within your CSV file. So this of course means it doesn't have to really be a CSV file. You can also provide a tab delimited file. The default constructor will specify a comma delimited file with fields enclosed by a double quote. If there is a double quote within a value it will be escaped by an additional double quote. This is as close to an accepted standard for CSV as there is.
Once your CSV data set class is instantiated you can use the method addTable() to add CSV files as tables to your data set. The addTable method takes two parameters. The first is $tableName and contains the name of the table you are adding. The second is $csvFile and contains the path to the CSV you will be using to set the data for that table. You can call addTable() for each table you would like to add to your data set.
In Example 9.5 you can see an example of how three CSV files can be combined into the database fixture for a database test case.
Example 9.5: CSV Data Set Example
--- fixture/post.csv ---
post_id,title,date_created,contents,rating
1,My First Post,2008-12-01 12:30:29,This is my first post,5
2,My Second Post,2008-12-04 15:35:25,This is my second post,
3,My Third Post,2008-12-09 03:17:05,This is my third post,3
--- fixture/post_comment.csv ---
post_comment_id,post_id,author,content,url
1,2,Tom,While the topic seemed interesting the content was lacking.,
2,2,Frank,That is because this is simply an example.,http://phpun.it
--- fixture/current_visitors.csv ---
current_visitors_id,ip
--- DatabaseTest.php ---
<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
require_once 'PHPUnit/Extensions/Database/DataSet/CsvDataSet.php';
class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getConnection()
{
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
return $this->createDefaultDBConnection($pdo, 'testdb');
}
protected function getDataSet()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
$dataSet->addTable('post', 'fixture/post.csv');
$dataSet->addTable('post_comment', 'fixture/post_comment.csv');
$dataSet->addTable('current_visitors', 'fixture/current_visitors.csv');
return $dataSet;
}
}
?>
Unfortunately, while the CSV dataset is appealing from the aspect of edit-ability, it has the same problems with NULL values as the flat XML dataset. There is no native way to explicitly specify a null value. If you do not specify a value (as I have done with some of the fields above) then the value will actually be set to that data type's equivalent of an empty string. Which in most cases will not be what you want. I will address this shortcoming of both the CSV and the flat XML data sets shortly.
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 10.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 10.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.4.2 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, Assertions: 0, Incomplete: 1.Table 10.1 shows the API for marking tests as incomplete.
Table 10.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 10.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 10.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.4.2 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, Assertions: 0, Skipped: 1.Table 10.2 shows the API for skipping tests.
Table 10.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. |
Gerard Meszaros introduces the concept of Test Doubles in [Meszaros2007] like this:
The getMock($className) method provided by PHPUnit can be
used in a test to automatically generate an object that can act as a test
double for the specified original class. This test double object can be used
in every context where an object of the original class is expected.
By default, all methods of the original class are replaced with a dummy
implementation that just returns NULL (without calling
the original method). Using the will($this->returnValue()
method, for instance, you can configure these dummy implementations to
return a value when called.
Please note that final, private
and static methods cannot be stubbed or mocked. They
are ignored by PHPUnit's test double functionality and retain their
original behavior.
The practice of replacing an object with a test double that (optionally) returns configured return values is refered to as stubbing. You can use a stub to "replace a real component on which the SUT depends so that the test has a control point for the indirect inputs of the SUT. This allows the test to force the SUT down paths it might not otherwise execute".
Example 11.2 shows how
to stub method calls and set up return values. We first use the
getMock() method that is provided by the
PHPUnit_Framework_TestCase class (see Table 22.6) to set up a stub object that looks
like an object of SomeClass
(Example 11.1). We then
use the Fluent Interface
that PHPUnit provides to specify the behavior for the stub. In essence,
this means that you do not need to create several temporary objects and
wire them together afterwards. Instead, you chain method calls as shown in
the example. This leads to more readable and "fluent" code.
Example 11.1: The class we want to stub
<?php
class SomeClass
{
public function doSomething()
{
// Do something.
}
}
?>
Example 11.2: Stubbing a method call to return a fixed value
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// Calling $stub->doSomething() will now return
// 'foo'.
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
"Behind the scenes", PHPUnit automatically generates a new PHP class that
implements the desired behavior when the getMock()
method is used. The generated test double class can be configured through
the optional arguments of the getMock() method.
By default, all methods of the given class are replaced with a test double that just returns NULL unless a return value is configured using will($this->returnValue(), for instance.
When the second (optional) parameter is provided, only the methods whose names are in the array are replaced with a configurable test double. The behavior of the other methods is not changed.
The third (optional) parameter may hold a parameter array that is passed to the original class' constructor (which is not replaced with a dummy implementation by default).
The fourth (optional) parameter can be used to specify a class name for the generated test double class.
The fifth (optional) parameter can be used to disable the call to the original class' constructor.
The sixth (optional) parameter can be used to disable the call to the original class' clone constructor.
The seventh (optional) parameter can be used to disable __autoload() during the generation of the test double class.
Sometimes you want to return one of the arguments of a method call
(unchanged) as the result of a stubbed method call.
Example 11.3 shows how you
can achieve this using returnArgument() instead of
returnValue().
Example 11.3: Stubbing a method call to return one of the arguments
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnArgumentStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnArgument(0));
// $stub->doSomething('foo') returns 'foo'
$this->assertEquals('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') returns 'bar'
$this->assertEquals('bar', $stub->doSomething('bar'));
}
}
?>
When the stubbed method call should return a calculated value instead of
a fixed one (see returnValue()) or an (unchanged)
argument (see returnArgument()), you can use
returnCallback() to have the stubbed method return the
result of a callback function or method. See
Example 11.4 for an example.
Example 11.4: Stubbing a method call to return a value from a callback
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnCallbackStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->returnCallback('str_rot13'));
// $stub->doSomething($argument) returns str_rot13($argument)
$this->assertEquals('fbzrguvat', $stub->doSomething('something'));
}
}
?>
Instead of returning a value, a stubbed method can also raise an
exception. Example 11.5
shows how to use throwException() to do this.
Example 11.5: Stubbing a method call to throw an exception
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testThrowExceptionStub()
{
// Create a stub for the SomeClass class.
$stub = $this->getMock('SomeClass');
// Configure the stub.
$stub->expects($this->any())
->method('doSomething')
->will($this->throwException(new Exception));
// $stub->doSomething() throws Exception
$stub->doSomething();
}
}
?>
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.
The practice of replacing an object with a test double that verifies expectations, for instance asserting that a method has been called, is refered to as mocking.
You can use a mock object "as an observation point that is used to verify the indirect outputs of the SUT as it is exercised. Typically, the mock object also includes the functionality of a test stub in that it must return values to the SUT if it hasn't already failed the tests but the emphasis is on the verification of the indirect outputs. Therefore, a mock object is lot more than just a test stub plus assertions; it is used a fundamentally different way".
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. Example 11.6
shows the code for the Subject and Observer
classes that are part of the System under Test (SUT).
Example 11.6: The Subject and Observer classes that are part of the System under Test (SUT)
<?php
class Subject
{
protected $observers = array();
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// Do something.
// ...
// Notify observers that we did something.
$this->notify('something');
}
protected function notify($argument)
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
// Other methods.
}
class Observer
{
public function update($argument)
{
// Do something.
}
// Other methods.
}
?>
Example 11.7
shows how to use a mock object to test the interaction between
Subject and Observer objects.
We first use the getMock() method that is provided by
the PHPUnit_Framework_TestCase class (see Table 22.6) 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.
Example 11.7: Testing that a methods gets called once and with a specified parameter
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testObserversAreUpdated()
{
// Create a mock for the Observer class,
// only mock 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 22.1 shows the constraints and Table 11.1 shows the matchers that are available.
Table 11.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 getMockForAbstractClass() method returns a mock
object for an abstract class. All abstract methods of the given abstract
class are mocked. This allows for testing the concrete methods of an
abstract class.
Example 11.8: Testing the concrete methods of an abstract class
<?php
abstract class AbstractClass
{
public function concreteMethod()
{
return $this->abstractMethod();
}
public abstract function abstractMethod();
}
class AbstractClassTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod()
{
$stub = $this->getMockForAbstractClass('AbstractClass');
$stub->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(TRUE));
$this->assertTrue($stub->concreteMethod());
}
}
?>
When your application interacts with a web service you want to test it
without actually interacting with the web service. To make the stubbing
and mocking of web services easy, the getMockFromWsdl()
can be used just like getMock() (see above). The only
difference is that getMockFromWsdl() returns a stub or
mock based on a web service description in WSDL and getMock()
returns a stub or mock based on a PHP class or interface.
Example 11.9
shows how getMockFromWsdl() can be used to stub, for
example, the web service described in GoogleSearch.wsdl.
Example 11.9: Stubbing a web service
<?php
class GoogleTest extends PHPUnit_Framework_TestCase
{
public function testSearch()
{
$googleSearch = $this->getMockFromWsdl(
'GoogleSearch.wsdl', 'GoogleSearch'
);
$directoryCategory = new StdClass;
$directoryCategory->fullViewableName = '';
$directoryCategory->specialEncoding = '';
$element = new StdClass;
$element->summary = '';
$element->URL = 'http://www.phpunit.de/';
$element->snippet = '...';
$element->title = '<b>PHPUnit</b>';
$element->cachedSize = '11k';
$element->relatedInformationPresent = TRUE;
$element->hostName = 'www.phpunit.de';
$element->directoryCategory = $directoryCategory;
$element->directoryTitle = '';
$result = new StdClass;
$result->documentFiltering = FALSE;
$result->searchComments = '';
$result->estimatedTotalResultsCount = 378000;
$result->estimateIsExact = FALSE;
$result->resultElements = array($element);
$result->searchQuery = 'PHPUnit';
$result->startIndex = 1;
$result->endIndex = 1;
$result->searchTips = '';
$result->directoryCategories = array();
$result->searchTime = 0.248822;
$googleSearch->expects($this->any())
->method('doGoogleSearch')
->will($this->returnValue($result));
/**
* $googleSearch->doGoogleSearch() will now return a stubbed result and
* the web service's doGoogleSearch() method will not be invoked.
*/
$this->assertEquals(
$result,
$googleSearch->doGoogleSearch(
'00000000000000000000000000000000',
'PHPUnit',
0,
1,
FALSE,
'',
FALSE,
'',
'',
''
)
);
}
}
?>
vfsStream is a stream wrapper for a virtual filesystem that may be helpful in unit tests to mock the real filesystem.
To install vfsStream, the PEAR channel
(pear.php-tools.net) that is used for
its distribution needs to be registered with the local PEAR environment:
pear channel-discover pear.php-tools.netThis has to be done only once. Now the PEAR Installer can be used to install vfsStream:
pear install pat/vfsStream-alphaExample 11.10 shows a class that interacts with the filesystem.
Example 11.10: A class that interacts with the filesystem
<?php
class Example
{
protected $id;
protected $directory;
public function __construct($id)
{
$this->id = $id;
}
public function setDirectory($directory)
{
$this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;
if (!file_exists($this->directory)) {
mkdir($this->directory, 0700, TRUE);
}
}
}?>
Without a virtual filesystem such as vfsStream we cannot test the
setDirectory() method in isolation from external
influence (see Example 11.11).
Example 11.11: Testing a class that interacts with the filesystem
<?php
require_once 'Example.php';
class ExampleTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
public function testDirectoryIsCreated()
{
$example = new Example('id');
$this->assertFalse(file_exists(dirname(__FILE__) . '/id'));
$example->setDirectory(dirname(__FILE__));
$this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
}
protected function tearDown()
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
}
?>
The approach above has several drawbacks:
As with any external resource, there might be intermittent problems with the filesystem. This makes tests interacting with it flaky.
In the setUp() and tearDown() methods we have to ensure that the directory does not exist before and after the test.
When the test execution terminates before the tearDown() method is invoked the directory will stay in the filesystem.
Example 11.12 shows how vfsStream can be used to mock the filesystem in a test for a class that interacts with the filesystem.
Example 11.12: Mocking the filesystem in a test for a class that interacts with the filesystem
<?php
require_once 'vfsStream/vfsStream.php';
require_once 'Example.php';
class ExampleTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
vfsStreamWrapper::register();
vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
}
public function testDirectoryIsCreated()
{
$example = new Example('id');
$this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));
$example->setDirectory(vfsStream::url('exampleDir'));
$this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
}
}
?>
This has several advantages:
The test itself is more concise.
vfsStream gives the test developer full control over what the filesystem environment looks like to the tested code.
Since the filesystem operations do not operate on the real filesystem anymore, cleanup operations in a tearDown() method are no longer required.
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-Driven Development. 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 section, 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.
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 13.1.
Example 13.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 13.2.
Example 13.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.4.2 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 13.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 13.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.4.2 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
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 13.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 13.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.
In [Astels2006], Dave Astels makes the following points:
Extreme Programming originally had the rule to test everything that could possibly break.
Now, however, the practice of testing in Extreme Programming has evolved into Test-Driven Development (see Chapter 13).
But the tools still force developers to think in terms of tests and assertions instead of specifications.
So if it's not about testing, what's it about? It's about figuring out what you are trying to do before you run off half-cocked to try to do it. You write a specification that nails down a small aspect of behaviour in a concise, unambiguous, and executable form. It's that simple. Does that mean you write tests? No. It means you write specifications of what your code will have to do. It means you specify the behaviour of your code ahead of time. But not far ahead of time. In fact, just before you write the code is best because that's when you have as much information at hand as you will up to that point. Like well done TDD, you work in tiny increments... specifying one small aspect of behaviour at a time, then implementing it. When you realize that it's all about specifying behaviour and not writing tests, your point of view shifts. Suddenly the idea of having a Test class for each of your production classes is ridiculously limiting. And the thought of testing each of your methods with its own test method (in a 1-1 relationship) will be laughable. | ||
| --Dave Astels | ||
The focus of Behaviour-Driven Development is "the language and interactions used in the process of software development. Behavior-driven developers use their native language in combination with the ubiquitous language of Domain-Driven Design to describe the purpose and benefit of their code. This allows the developers to focus on why the code should be created, rather than the technical details, and minimizes translation between the technical language in which the code is written and the domain language spoken by the" domain experts.
The PHPUnit_Extensions_Story_TestCase class adds a story
framework that faciliates the definition of a
Domain-Specific Language
for Behaviour-Driven Development. Inside a scenario,
given(), when(), and
then() each represent a step.
and() is the same kind as the previous step. The following
methods are declared abstract in
PHPUnit_Extensions_Story_TestCase and need to be
implemented:
runGiven(&$world, $action, $arguments)
...
runWhen(&$world, $action, $arguments)
...
runThen(&$world, $action, $arguments)
...
In this section, we will look at the example of a class that calculates the score for a game of bowling. The rules for this are as follows:
The game consists of 10 frames.
In each frame the player has two opportunities to knock down 10 pins.
The score for a frame is the total number of pins knocked down, plus bonuses for strikes and spares.
A spare is when the player knocks down all 10 pins in two tries.
The bonus for that frame is the number of pins knocked down by the next roll.
A strike is when the player knocks down all 10 pins on his first try.
The bonus for that frame is the value of the next two balls rolled.
Example 14.1
shows how the above rules can be written down as specification scenarios
using PHPUnit_Extensions_Story_TestCase.
Example 14.1: Specification for the BowlingGame class
<?php
require_once 'PHPUnit/Extensions/Story/TestCase.php';
require_once 'BowlingGame.php';
class BowlingGameSpec extends PHPUnit_Extensions_Story_TestCase
{
/**
* @scenario
*/
public function scoreForGutterGameIs0()
{
$this->given('New game')
->then('Score should be', 0);
}
/**
* @scenario
*/
public function scoreForAllOnesIs20()
{
$this->given('New game')
->when('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->then('Score should be', 20);
}
/**
* @scenario
*/
public function scoreForOneSpareAnd3Is16()
{
$this->given('New game')
->when('Player rolls', 5)
->and('Player rolls', 5)
->and('Player rolls', 3)
->then('Score should be', 16);
}
/**
* @scenario
*/
public function scoreForOneStrikeAnd3And4Is24()
{
$this->given('New game')
->when('Player rolls', 10)
->and('Player rolls', 3)
->and('Player rolls', 4)
->then('Score should be', 24);
}
/**
* @scenario
*/
public function scoreForPerfectGameIs300()
{
$this->given('New game')
->when('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->then('Score should be', 300);
}
public function runGiven(&$world, $action, $arguments)
{
switch($action) {
case 'New game': {
$world['game'] = new BowlingGame;
$world['rolls'] = 0;
}
break;
default: {
return $this->notImplemented($action);
}
}
}
public function runWhen(&$world, $action, $arguments)
{
switch($action) {
case 'Player rolls': {
$world['game']->roll($arguments[0]);
$world['rolls']++;
}
break;
default: {
return $this->notImplemented($action);
}
}
}
public function runThen(&$world, $action, $arguments)
{
switch($action) {
case 'Score should be': {
for ($i = $world['rolls']; $i < 20; $i++) {
$world['game']->roll(0);
}
$this->assertEquals($arguments[0], $world['game']->score());
}
break;
default: {
return $this->notImplemented($action);
}
}
}
}
?>
phpunit --story BowlingGameSpec
PHPUnit 3.4.2 by Sebastian Bergmann.
BowlingGameSpec
[x] Score for gutter game is 0
Given New game
Then Score should be 0
[x] Score for all ones is 20
Given New game
When Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
Then Score should be 20
[x] Score for one spare and 3 is 16
Given New game
When Player rolls 5
and Player rolls 5
and Player rolls 3
Then Score should be 16
[x] Score for one strike and 3 and 4 is 24
Given New game
When Player rolls 10
and Player rolls 3
and Player rolls 4
Then Score should be 24
[x] Score for perfect game is 300
Given New game
When Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
Then Score should be 300
Scenarios: 5, Failed: 0, Skipped: 0, Incomplete: 0.The beauty of testing is found not in the effort but in the effiency. Knowing what should be tested is beautiful, and knowing what is being tested is beautiful. | ||
| --Murali Nandigama | ||
In this chapter you will learn all about PHPUnit's code coverage functionality that provides an insight into what parts of the production code are executed when the tests are run. It helps answering questions such as:
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?
An example of what code coverage statistics can mean 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.
PHPUnit's code coverage functionality makes use of the Xdebug extension for PHP.
Let us generate a code coverage report for the BankAccount
class from Example 13.3.
phpunit --coverage-html ./report BankAccountTest
PHPUnit 3.4.2 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
Generating report, this may take a moment.Figure 15.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 grey. The number left to the actual line of code indicates how many tests cover that line.
Clicking on the line number of a covered line will open a panel (see Figure 15.2) that shows the test cases that cover this 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 15.1
shows a test that can be added to the BankAccountTest
test case class to completely cover the BankAccount
class.
Example 15.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 15.3 shows
the code coverage of the setBalance() method with the
additional test.
The @covers annotation (see
Table B.1) can be
used in the test code to specify which method(s) a test method wants to
test. If provided, only the code coverage information for the specified
method(s) will be considered.
Example 15.2
shows an example.
Example 15.2: Tests that specify which method they want to cover
<?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;
}
/**
* @covers BankAccount::getBalance
*/
public function testBalanceIsInitiallyZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
/**
* @covers BankAccount::withdrawMoney
*/
public function testBalanceCannotBecomeNegative()
{
try {
$this->ba->withdrawMoney(1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
/**
* @covers BankAccount::depositMoney
*/
public function testBalanceCannotBecomeNegative2()
{
try {
$this->ba->depositMoney(-1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
/**
* @covers BankAccount::getBalance
* @covers BankAccount::depositMoney
* @covers BankAccount::withdrawMoney
*/
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());
}
}
?>
Sometimes you have blocks of code that you cannot test and that you may
want to ignore during code coverage analysis. PHPUnit lets you do this
using the @codeCoverageIgnoreStart and
@codeCoverageIgnoreEnd annotations as shown in
Example 15.3.
Example 15.3: Using the @codeCoverageIgnoreStart and @codeCoverageIgnoreEnd annotations
<?php
class Sample
{
// ...
public function doSomething()
{
if (0) {
// @codeCoverageIgnoreStart
$this->doSomethingElse();
// @codeCoverageIgnoreEnd
}
}
// ...
}
?>
The lines of code between the @codeCoverageIgnoreStart
and @codeCoverageIgnoreEnd annotations are counted as
executed (if they are executable) and will not be highlighted.
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. The sourcecode files that are included in the report can be filtered by using a blacklist or a whitelist approach.
The blacklist is pre-filled with all sourcecode files of PHPUnit itself as well as 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 optionally added to the code coverage report regardless of whether or not it was executed.
PHPUnit's XML configuration file (see the section called “Including and Excluding Files for Code Coverage”) can be used to control the blacklist and the whitelist. Using a whitelist is the recommended best practice to control the list of files included in the code coverage report.
Alternatively, you can configure the sourcecode files that are included in
the report using the PHPUnit_Util_Filter API.
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 13.1):
phpunit --testdox BankAccountTest
PHPUnit 3.4.2 by Sebastian Bergmann.
BankAccount
[x] Balance is initially zero
[x] 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.
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 17.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 17.1).
phpunit --skeleton Calculator
PHPUnit 3.4.2 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 10) in the generated test case class.
When you are using the skeleton generator to generate code based on a class that is declared in a namespace you have to provide the qualified name of the class as well as the path to the source file it is declared in.
For instance, for a class Bar that is declared in the
Foo namespace you need to invoke the skeleton
generator like this:
phpunit --skeleton-test Foo\Bar Bar.php
Below is the output of running the generated test case class.
phpunit --verbose CalculatorTest
PHPUnit 3.4.2 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, Assertions: 0, 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 17.2
shows an example.
Example 17.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.4.2 by Sebastian Bergmann.
....
Time: 0 seconds
OK (4 tests, 4 assertions)
Table 17.1
shows the supported variations of the @assert
annotation and how they are transformed into test code.
Table 17.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 | assertGreaterThan(X, method(...)) |
@assert (...) >= X | assertGreaterThanOrEqual(X, method(...)) |
@assert (...) < X | assertLessThan(X, method(...)) |
@assert (...) <= X | assertLessThanOrEqual(X, method(...)) |
@assert (...) throws X | @expectedException X |
When you are doing Test-Driven Development (see Chapter 13) and write your tests before the code that the tests exercise, PHPUnit can help you generate class skeletons from test case classes.
Following the convention that the tests for a class Unit
are written in a class named UnitTest, the test case
class' source is searched for variables that reference objects of the
Unit class and analyzing what methods are called on
these objects. For example, take a look at Example 17.4 which has
been generated based on the analysis of Example 17.3.
Example 17.3: The BowlingGameTest class
<?php
class BowlingGameTest extends PHPUnit_Framework_TestCase
{
protected $game;
protected function setUp()
{
$this->game = new BowlingGame;
}
protected function rollMany($n, $pins)
{
for ($i = 0; $i < $n; $i++) {
$this->game->roll($pins);
}
}
public function testScoreForGutterGameIs0()
{
$this->rollMany(20, 0);
$this->assertEquals(0, $this->game->score());
}
}
?>
Example 17.4: The generated BowlingGame class skeleton
<?php
/**
* Generated by PHPUnit on 2008-03-10 at 17:18:33.
*/
class BowlingGame
{
/**
* @todo Implement roll().
*/
public function roll()
{
// Remove the following line when you implement this method.
throw new RuntimeException('Not yet implemented.');
}
/**
* @todo Implement score().
*/
public function score()
{
// Remove the following line when you implement this method.
throw new RuntimeException('Not yet implemented.');
}
}
?>
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 18.1 shows
how to test the contents of the <title>
element of the http://www.example.com/
website.
Example 18.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->assertTitle('Example WWW Page');
}
}
?>
phpunit WebTest
PHPUnit 3.4.2 by Sebastian Bergmann.
F
Time: 5 seconds
There was 1 failure:
1) testTitle(WebTest)
Current URL: http://www.example.com/
Failed asserting that two strings are equal.
expected string <Example WWW Page>
difference < xx>
got string <Example Web Page>
/home/sb/WebTest.php:30
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
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 18.1
for the list of methods that are available for this.
Table 18.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. |
PHPUnit can optionally capture a screenshot when a Selenium test fails. To
enable this, set $captureScreenshotOnFailure,
$screenshotPath, and $screenshotUrl
in your test case class as shown in
Example 18.2.
Example 18.2: Capturing a screenshot when a test fails
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected $captureScreenshotOnFailure = TRUE;
protected $screenshotPath = '/var/www/localhost/htdocs/screenshots';
protected $screenshotUrl = 'http://localhost/screenshots';
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example WWW Page');
}
}
?>
phpunit WebTest
PHPUnit 3.4.2 by Sebastian Bergmann.
F
Time: 5 seconds
There was 1 failure:
1) testTitle(WebTest)
Current URL: http://www.example.com/
Screenshot: http://localhost/screenshots/6c8e1e890fff864bd56db436cd0f309e.png
Failed asserting that two strings are equal.
expected string <Example WWW Page>
difference < xx>
got string <Example Web Page>
/home/sb/WebTest.php:30
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
You can run each test using a set of browsers: Instead of using
setBrowser() to set up one browser you declare a
public static array named $browsers
in your test case class. Each item in this array describes one browser
configuration. Each of these browsers can be hosted by different
Selenium RC servers.
Example 18.3 shows
an example.
Example 18.3: Setting up multiple browser configurations
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
public static $browsers = array(
array(
'name' => 'Firefox on Linux',
'browser' => '*firefox',
'host' => 'my.linux.box',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Safari on MacOS X',
'browser' => '*safari',
'host' => 'my.macosx.box',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Safari on Windows XP',
'browser' => '*custom C:\Program Files\Safari\Safari.exe -url',
'host' => 'my.windowsxp.box',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Internet Explorer on Windows XP',
'browser' => '*iexplore',
'host' => 'my.windowsxp.box',
'port' => 4444,
'timeout' => 30000,
)
);
protected function setUp()
{
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example Web Page');
}
}
?>
PHPUnit_Extensions_SeleniumTestCase can collect code
coverage information for tests run through Selenium:
PHPUnit/Extensions/SeleniumTestCase/phpunit_coverage.php into your webserver's document root directory.php.ini configuration file, configure PHPUnit/Extensions/SeleniumTestCase/prepend.php and PHPUnit/Extensions/SeleniumTestCase/append.php as the auto_prepend_file and auto_append_file, respectively.PHPUnit_Extensions_SeleniumTestCase, use protected $coverageScriptUrl = 'http://host/phpunit_coverage.php'; to configure the URL for the phpunit_coverage.php script.
Table 18.2 lists the
various assertion methods that PHPUnit_Extensions_SeleniumTestCase
provides.
Table 18.2. Assertions
| Assertion | Meaning |
|---|---|
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 assertElementValueContains(string $locator, string $text) | Reports an error if the value of the element identified by $locator does not contain the given $text. |
void assertElementValueContains(string $locator, string $text) | Reports an error if the value of the element identified by $locator contains 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 assertSelectHasOption(string $selectLocator, string $option) | Reports an error if the given option is not available. |
void assertSelectNotHasOption(string $selectLocator, string $option) | Reports an error if the given option is available. |
void assertSelected($selectLocator, $option) | Reports an error if the given label is not selected. |
void assertNotSelected($selectLocator, $option) | Reports an error if the given label is selected. |
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. |
Table 18.3 shows
the template method of PHPUnit_Extensions_SeleniumTestCase:
Table 18.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. |
Please refer to the documentation of Selenium commands for a reference of the commands available and how they are used.
Using the runSelenese($filename) method, you can also
run a Selenium test from its Selenese/HTML specification. Furthermore,
using the static attribute $seleneseDirectory, you can
automatically create test objects from a directory that contains
Selenese/HTML files. The specified directory is recursively searched for
.htm files that are expected to contain Selenese/HTML.
Example 18.4 shows an
example.
Example 18.4: Use a directory of Selenese/HTML files as tests
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class SeleneseTests extends PHPUnit_Extensions_SeleniumTestCase
{
public static $seleneseDirectory = '/path/to/files';
}
?>
PHPUnit can produce several types of logfiles.
The XML logfile for test results produced by PHPUnit is 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"
assertions="2"
failures="0"
errors="0"
time="0.016030">
<testcase name="testNewArrayIsEmpty"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="6"
assertions="1"
time="0.008044"/>
<testcase name="testArrayContainsAnElement"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="15"
assertions="1"
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"
assertions="1"
failures="1"
errors="1"
time="0.019744">
<testcase name="testFailure"
class="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
line="6"
assertions="1"
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"
assertions="0"
time="0.008288">
<error type="Exception">testError(FailureErrorTest)
Exception:
/home/sb/FailureErrorTest.php:13
</error>
</testcase>
</testsuite>
</testsuites>
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:
TAP version 13 ok 1 - testNewArrayIsEmpty(ArrayTest) ok 2 - testArrayContainsAnElement(ArrayTest) 1..2
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.
TAP version 13
not ok 1 - Failure: testFailure(FailureErrorTest)
---
message: 'Failed asserting that <integer:2> matches expected value <integer:1>.'
severity: fail
data:
got: 2
expected: 1
...
not ok 2 - Error: testError(FailureErrorTest)
1..2
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.0082459449768066,"trace":[],
"message":"Failed asserting that <integer:2> is equal to <integer:1>."}
{"event":"test","suite":"FailureErrorTest",
"test":"testError(FailureErrorTest)","status":"error",
"time":0.0083680152893066,"trace":[],"message":""}
The XML format for code coverage information logging produced 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.4.2">
<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>This chapter provides an overview of build automation tools that are commonly used with PHPUnit.
Apache Ant is a Java-based build tool. In theory, it is kind of like make, without make's wrinkles. Build files for Apache Ant are XML-based, calling out a target tree where various tasks get executed.
Example 20.1 shows an
Apache Ant build.xml file that invokes PHPUnit using the built-in
<exec> task. The build process is aborted
if a test fails (failonerror="true").
Example 20.1: Apache Ant build.xml file that invokes PHPUnit
<project name="Money" default="build">
<target name="clean">
<delete dir="${basedir}/build"/>
</target>
<target name="prepare">
<mkdir dir="${basedir}/build/logs"/>
</target>
<target name="phpunit">
<exec dir="${basedir}" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/phpunit.xml MoneyTest" />
</exec>
</target>
<target name="build" depends="clean,prepare,phpunit"/>
</project>ant
Buildfile: build.xml
clean:
prepare:
[mkdir] Created dir: /home/sb/Money/build/logs
phpunit:
[exec] PHPUnit 3.4.2 by Sebastian Bergmann.
[exec]
[exec] ......................
[exec]
[exec] Time: 0 seconds
[exec]
[exec] OK (22 tests, 34 assertions)
build:
BUILD SUCCESSFUL
Total time: 0 seconds
The XML logfile for test results produced by PHPUnit (see the section called “Test Results (XML)”) is based upon the one used by the <junit>
task that comes with Apache Ant.
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. Maven for PHP uses the power of Maven for building, testing, and documenting PHP projects.
Phing (PHing Is Not GNU make) is a project build system based on Apache Ant. You can do anything with it that you could do with a traditional build system such as GNU make, and its use of simple XML build files and extensible PHP "task" classes make it an easy-to-use and highly flexible build framework. Features include file transformations (e.g. token replacement, XSLT transformation, Smarty template transformations), file system operations, interactive build support, SQL execution, CVS operations, tools for creating PEAR packages, and much more.
Example 20.2 shows a Phing
build.xml file that invokes PHPUnit using the built-in
<phpunit> task. The build process is
aborted if a test fails (haltonfailure="true").
Example 20.2: Phing build.xml file that invokes PHPUnit
<project name="Money" default="build">
<target name="clean">
<delete dir="build"/>
</target>
<target name="prepare">
<mkdir dir="build/logs"/>
</target>
<target name="phpunit">
<phpunit printsummary="true" haltonfailure="true">
<formatter todir="build/logs" type="xml"/>
<batchtest>
<fileset dir=".">
<include name="*Test.php"/>
</fileset>
</batchtest>
</phpunit>
</target>
<target name="build" depends="clean,prepare,phpunit"/>
</project>phing
Buildfile: /home/sb/Money/build.xml
Money > clean:
Money > prepare:
[mkdir] Created dir: /home/sb/Money/build/logs
Money > phpunit:
[phpunit] Test: MoneyTest, Run: 22, Failures: 0, Errors: 0,
Incomplete: 0, Skipped: 0, Time elapsed: 0.06887 s
Money > build:
BUILD FINISHED
Total time: 0.2632 secondsThis chapter provides an overview of Continuous Integration summarizing the technique and its application with PHPUnit.
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 | ||
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.
Atlassian Bamboo is a Continuous Integration (CI) server that assists software development teams by providing automated building and testing of software source-code status, updates on successful/failed builds, and reporting tools for statistical analysis.
The following example assumes that the Bamboo distribution archive has
been unpacked into /usr/local/Bamboo.
cd /usr/local/BambooEdit the webapp/WEB-INF/classes/bamboo-init.properties file.
Optionally install the bamboo-checkstyle plugin.
./bamboo.sh startOpen http://localhost:8085/ in your webbrowser.
Follow the guided installation instructions.
Configure Apache Ant as a Builder in the Administration panel.
Bamboo is now configured and we can set up a project plan. First we need
a project, though. For the purpose of this example lets assume we have a
copy of the Money sample that ships with PHPUnit in a
Subversion repository (file:///var/svn/money).
Together with the *.php files we also have the
following Apache Ant build script (build.xml) in the
repository.
Example 21.2: build.xml
<project name="Money" default="build">
<target name="clean">
<delete dir="${basedir}/build"/>
</target>
<target name="prepare">
<mkdir dir="${basedir}/build"/>
<mkdir dir="${basedir}/build/logs"/>
</target>
<target name="phpcs">
<exec dir="${basedir}"
executable="phpcs"
output="${basedir}/build/logs/checkstyle.xml"
failonerror="false">
<arg line="--report=checkstyle ." />
</exec>
</target>
<target name="phpunit">
<exec dir="${basedir}" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/phpunit.xml
--coverage-clover ${basedir}/build/logs/clover.xml
--coverage-html ${basedir}/build/coverage
MoneyTest" />
</exec>
</target>
<target name="build" depends="clean,prepare,phpcs,phpunit"/>
</project>Now that we have a project, we can create a plan for it in Bamboo.
Open http://localhost:8080/ in your webbrowser.
Follow the guided "Create a Plan" instructions.
In step 3 of "Create a Plan", check the "The build will produce test results" and "Clover output will be produced" options and provide the paths to the XML files produced by PHPUnit.
If you installed the bamboo-checkstyle plugin also check the "CheckStyle output will be produced" option and provide the path of the XML file produced by PHP_CodeSniffer.
In step 5 of "Create a Plan", set up an artifact for the HTML files (*.*, build/coverage) that PHPUnit produces.
CruiseControl is a framework for continuous build processes and 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/Money/build/logscd projects/Moneysvn co file:///var/svn/money sourceEdit the build.xml file.
Example 21.3: projects/Money/build.xml
<project name="Money" 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/phpunit.xml MoneyTest"/>
</exec>
</target>
<target name="build" depends="checkout,test"/>
</project>cd /usr/local/cruisecontrolEdit the config.xml file.
Example 21.4: config.xml
<cruisecontrol>
<project name="Money" 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>Now we can (re)start the CruiseControl server.
./cruisecontrol.shOpen http://localhost:8080/ in your webbrowser.
phpUnderControl is an
extension for CruiseControl that integrates several PHP development tools,
such as PHPUnit for testing,
PHP_CodeSniffer
for static
code analysis, and PHPDocumentor
for API
documentation generation. It comes with a powerful command-line
tool that can, among other things, automatically create CruiseControl's
XML configuration files for your project. The following example assumes
that CruiseControl has been installed into
/usr/local/cruisecontrol.
pear install --alldeps phpunit/phpUnderControlphpuc install /usr/local/cruisecontrolphpuc project --version-control svn
--version-control-url file:///var/svn/money
--test-case MoneyTest
--test-file MoneyTest.php
--test-dir .
--project-name Money
/usr/local/cruisecontrol
The above command creates the project directory and the project's
build.xml configuration file, performs the initial
checkout from the source repository, and adds the new project to the
global config.xml configuration file. Now we can
(re)start the CruiseControl server.
cd /usr/local/cruisecontrol./cruisecontrol.shOpen http://localhost:8080/ in your webbrowser.
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 22.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 22.1).
Example 22.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 22.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 22.2, using
assertions with messages:
phpunit MessageTest
PHPUnit 3.4.4 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, Assertions: 1, Failures: 1.Below are all the varieties of assertions.
assertArrayHasKey(mixed $key, array $array[, string $message = ''])
Reports an error identified by $message if $array does not have the $key.
assertArrayNotHasKey() is the inverse of this assertion and takes the same arguments.
Example 22.3: Usage of assertArrayHasKey()
<?php
class ArrayHasKeyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertArrayHasKey('foo', array('bar' => 'baz'));
}
}
?>
phpunit ArrayHasKeyTest
PHPUnit 3.4.4 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) ArrayHasKeyTest::testFailure
Failed asserting that an array has the key <string:foo>.
/home/sb/ArrayHasKeyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasAttribute(string $attributeName, string $className[, string $message = ''])
Reports an error identified by $message if $className::attributeName does not exist.
assertClassNotHasAttribute() is the inverse of this assertion and takes the same arguments.
Example 22.4: Usage of assertClassHasAttribute()
<?php
class ClassHasAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertClassHasAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassHasAttributeTest
PHPUnit 3.4.4 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) ClassHasAttributeTest::testFailure
Failed asserting that class "stdClass" has attribute "foo".
/home/sb/ClassHasAttributeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasStaticAttribute(string $attributeName, string $className[, string $message = ''])
Reports an error identified by $message if $className::attributeName does not exist.
assertClassNotHasStaticAttribute() is the inverse of this assertion and takes the same arguments.
Example 22.5: Usage of assertClassHasStaticAttribute()
<?php
class ClassHasStaticAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertClassHasStaticAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassHasStaticAttributeTest
PHPUnit 3.4.4 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) ClassHasStaticAttributeTest::testFailure
Failed asserting that class "stdClass" has static attribute "foo".
/home/sb/ClassHasStaticAttributeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])
Reports an error identified by $message if $needle is not an element of $haystack.
assertNotContains() is the inverse of this assertion and takes the same arguments.
assertAttributeContains() and assertAttributeNotContains() are convenience wrappers that use a public, protected, or private attribute of a class or object as the haystack.
Example 22.6: Usage of assertContains()
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContains(4, array(1, 2, 3));
}
}
?>
phpunit ContainsTest
PHPUnit 3.4.4 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) ContainsTest::testFailure
Failed asserting that an array contains <integer:4>.
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(string $needle, string $haystack[, string $message = ''])
Reports an error identified by $message if $needle is not a substring of $haystack.
Example 22.7: Usage of assertContains()
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContains('baz', 'foobar');
}
}
?>
phpunit ContainsTest
PHPUnit 3.4.4 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) ContainsTest::testFailure
Failed asserting that <string:foobar> contains "baz".
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])
Reports an error identified by $message if $haystack does not contain only variables of type $type.
$isNativeType is a flag used to indicate whether $type is a native PHP type or not.
assertNotContainsOnly() is the inverse of this assertion and takes the same arguments.
assertAttributeContainsOnly() and assertAttributeNotContainsOnly() are convenience wrappers that use a public, protected, or private attribute of a class or object as the actual value.
Example 22.8: Usage of assertContainsOnly()
<?php
class ContainsOnlyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContainsOnly('string', array('1', '2', 3));
}
}
?>
phpunit ContainsOnlyTest
PHPUnit 3.4.4 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) ContainsOnlyTest::testFailure
Failed asserting that
Array
(
[0] => 1
[1] => 2
[2] => 3
)
contains only values of type "string".
/home/sb/ContainsOnlyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEqualXMLStructure(DOMNode $expectedNode, DOMNode $actualNode[, boolean $checkAttributes = FALSE, string $message = ''])
XXX
assertEquals(mixed $expected, mixed $actual[, string $message = ''])
Reports an error identified by $message if the two variables $expected and $actual are not equal.
assertNotEquals() is the inverse of this assertion and takes the same arguments.
assertAttributeEquals() and assertAttributeNotEquals() are convenience wrappers that use a public, protected, or private attribute of a class or object as the actual value.