Приглашаем посетить
Анненский (annenskiy.lit-info.ru)

Section 9-1.  BankAccount Example

Previous
Table of Contents
Next

9-1. BankAccount Example

In this section, we will look at the example of a class that represents a bank account. The contract for the BankAccount class requires methods to get and set the bank account's balance, as well as methods to deposit and withdraw money. It also specifies that the following two conditions 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 10.

Example 10. 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 11.

Example 11. 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 12. These methods are written in such a way that they raise an InvalidArgumentException when they are called with illegal values that would violate the contract conditions.

Example 12. 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 13. When one of these assertions fails, a 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 13. 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.


Previous
Table of Contents
Next