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.
What follows is necessarily an abbreviated introduction to Test-First Programming. You can explore the topic further in other books, such as Test-Driven Development [Beck2002] by Kent Beck or Dave Astels' A Practical Guide to Test-Driven Development [Astels2003].
In this chapter, we will look at the example of a class that represents
a bank account. The contract for the BankAccount
class not only requires methods to get and set the bank account's
balance, as well as methods to deposit and withdraw money. It also
specifies the following two conditions that must be ensured:
The bank account's initial balance must be zero.
The bank account's balance cannot become negative.
Following the Test-First Programming approach, we write the tests for
the BankAccount
class before we write the code for
the class itself. We use the contract conditions as the basis for the
tests and name the test methods accordingly, as shown in
Example 8.1.
Example 8.1: Tests for the BankAccount class
<?php
require_once 'PHPUnit2/Framework/TestCase.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit2_Framework_TestCase {
private $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 (Exception $e) {
return;
}
$this->fail();
}
public function testBalanceCannotBecomeNegative2() {
try {
$this->ba->depositMoney(-1);
}
catch (Exception $e) {
return;
}
$this->fail();
}
public function testBalanceCannotBecomeNegative3() {
try {
$this->ba->setBalance(-1);
}
catch (Exception $e) {
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 8.2.
Example 8.2: Code needed for the testBalanceIsInitiallyZero() test to pass
<?php
class BankAccount {
private $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 2.3.0 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 8.3.
These methods are written in a such a way that they raise an
InvalidArgumentException
when they are called with
illegal values that would violate the contract conditions.
Example 8.3: The complete BankAccount class
<?php
class BankAccount {
private $balance = 0;
public function getBalance() {
return $this->balance;
}
public function setBalance($balance) {
if ($balance >= 0) {
$this->balance = $balance;
} else {
throw new InvalidArgumentException;
}
}
public function depositMoney($amount) {
if ($amount >= 0) {
$this->balance += $amount;
} else {
throw new InvalidArgumentException;
}
}
public function withdrawMoney($amount) {
if ($amount >= 0 && $this->balance >= $amount) {
$this->balance -= $amount;
} else {
throw new InvalidArgumentException;
}
}
}
?>
The tests that ensure the second contract condition now pass, too:
phpunit BankAccountTest
PHPUnit 2.3.0 by Sebastian Bergmann.
....
Time: 0.057038
OK (4 tests)
Alternatively, you can use the static assertion methods provided by the
PHPUnit2_Framework_Assert
class to write the contract
conditions as design-by-contract style assertions into your code, as
shown in Example 8.4.
When one of these assertions fails, an
PHPUnit2_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 8.4: The BankAccount class with Design-by-Contract assertions
<?php
require_once 'PHPUnit2/Framework/Assert.php';
class BankAccount {
private $balance = 0;
public function getBalance() {
return $this->balance;
}
public function setBalance($balance) {
PHPUnit2_Framework_Assert::assertTrue($balance >= 0);
$this->balance = $balance;
}
public function depositMoney($amount) {
PHPUnit2_Framework_Assert::assertTrue($amount >= 0);
$this->balance += $amount;
}
public function withdrawMoney($amount) {
PHPUnit2_Framework_Assert::assertTrue($amount >= 0);
PHPUnit2_Framework_Assert::assertTrue($this->balance >= $amount);
$this->balance -= $amount;
}
}
?>
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.