Prev Next

第12章 テスト駆動開発

テストファーストプログラミング、 エクストリームプログラミング、 そして テスト駆動開発 などのソフトウェア開発方法論において、ユニットテストは非常に重要な位置を占めています。 また、構造上この手法に対応できない言語については 規約による設計 (Design-by-Contract) という手法も認めています。

プログラムを書き終えてから PHPUnit でテストを書くこともできます。 が、テストを書き始めるのが早ければ早いほど、テストの価値が高くなります。 コードが「完成」して何ヶ月もたってからテストを書き始めるのではなく、数日後、 数時間後、いやもうひとがんばりして数分後に書き始めることだってできるでしょう。 さらにもう一歩先へ進んでみませんか? コードを書き始める前にテストを書いたっていいんじゃないですか?

エクストリームプログラミングやテスト駆動開発における 「テストファーストプログラミング」はこの考えに基づいたもので、 さらにそれを究極まで推し進めたものです。現在のコンピュータの能力をもってすれば、 一日に何千ものテストを何千回も繰り返すことだって可能です。 これらのテスト結果を活用することで、 プログラムを少しずつ確実に作成することができるようになります。 テストを自動化すると、新しく追加したテストだけでなく これまでのテストもすべて実行できることが保証されるのです。 テストとはハーケン (登山のときにザイルを通したりする頭部に穴の開いた鋼鉄製の釘) のようなもので、何が起ころうともこの段階までは確実に完成しているということを保証してくれます。

最初にテストを書き始めたときは、おそらくそれを実行できないでしょう。 だって、まだ実装していないオブジェクトやメソッドを使用しているのだから。 最初のうちはこれを気持ち悪く感じるかもしれません。でもそのうちに慣れてきます。 テストファーストプログラミングというのは、オブジェクト指向開発の原則である 「実装をプログラミングするのではなくインターフェイスをプログラミングする」 に従うための実践的な手法であると考えましょう。テストを書いている間、 あなたはきっとテスト対象オブジェクトのインターフェイス (このオブジェクトは、 外部からはどのように見えるのか) について考えていることでしょう。 テストが実際に動作するようになったら、そこで実装のことを考え始めます。 出来上がったテストによって、この段階でインターフェイスは確定しています。

 

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

この後に続くテスト駆動開発の例は、やむを得ず簡潔なものになっています。 詳細については Kent Beck の Test-Driven Development [Beck2002] [Beck2002-ja] や Dave Astels の A Practical Guide to Test-Driven Development [Astels2003] などの書籍を参照ください。

銀行口座の例

この節では、銀行口座を表すクラスを例にして考えます。預金残高の取得や設定、 預け入れや引き落としなどのメソッドだけでなく、BankAccount クラスは以下のふたつの規約を満たす必要があります。

  • 預金残高の初期値はゼロでなければならない。

  • 預金残高がゼロ未満になってはならない。

まずは BankAccount クラスのテストを作成し、その後で実際のコードを書いていくようにしましょう。 上の規約をテスト作成の基準とし、それにしたがって 例 12.1 のようにテストメソッドの名前をつけます。

例 12.1: BankAccount クラスのテスト

<?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();
}
}
?>

それでは、最初のテスト testBalanceIsInitiallyZero() をクリアするために必要な最小限のコードを書いていきましょう。必要なのは、 BankAccount クラスの getBalance() メソッドを 例 12.2 のように実装することです。

例 12.2: テスト testBalanceIsInitiallyZero() をクリアするために必要なコード

<?php
class BankAccount
{
protected $balance = 0;

public function getBalance()
{
return $this->balance;
}
}
?>

これで最初のテストはクリアすることになりましたが、 2 番目のテストには失敗します。なぜなら、 テストメソッド内でコールしているメソッドがまだ実装されていないからです。

phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.

.
Fatal error: Call to undefined method BankAccount::withdrawMoney()

ふたつめの規約のテストをクリアするには、withdrawMoney()depositMoney() および setBalance() の各メソッドを 例 12.3 のように実装しなければなりません。これらのメソッドは、 規約に反するような引数でコールされた場合には BankAccountException を発生させるように実装しています。

例 12.3: 完全な BankAccount クラス

<?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();
}
}
?>

これで、2 つめの規約に関するテストにもクリアするようになります。

phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.

...

Time: 0 seconds


OK (3 tests, 3 assertions)

別の方法としては、PHPUnit_Framework_Assert クラスが提供する静的なアサーションメソッドを用いて、コード内に 「規約による設計」方式のアサーションを記述するというものもあります。 例 12.4 がその例です。これらのアサーションのいずれかに失敗すると、例外 PHPUnit_Framework_AssertionFailedError が発生します。 この方式を用いると、条件チェックのコードを減らすことができてテストが読みやすくなります。 ただ、プログラムの実行時にも PHPUnit が必要になってしまいます。

例 12.4: 「規約による設計」のアサーションを使用した BankAccount クラス

<?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();
}
}
?>

規約を満たすための条件をテスト内に記述することで、「規約による設計」 方式で BankAccount クラスをプログラミングしてきました。 次に、テストファーストプログラミングの考え方にしたがって、 テストをクリアするために必要なコードを記述してきました。 でも、ひとつ忘れてしまったことがあります。それは、 setBalance()depositMoney() および withdrawMoney() に正当な値を指定した場合に、 正常に動作することを確かめるテストを書くことです。 自分が書いたテストが妥当なものなのか、 それで十分なのかを調べるためのテストが必要ですね。次の章では、そのための 「コードカバレッジ解析」について説明します。

Prev Next
1. 自動テスト
2. PHPUnit の目標
3. PHPUnit のインストール
PEAR
Composer
PHP Archive (PHAR)
オプションのパッケージ
アップグレード
4. PHPUnit 用のテストの書き方
テストの依存性
データプロバイダ
例外のテスト
PHP のエラーのテスト
出力内容のテスト
アサーション
assertArrayHasKey()
assertClassHasAttribute()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertContainsOnlyInstancesOf()
assertCount()
assertEmpty()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertGreaterThan()
assertGreaterThanOrEqual()
assertInstanceOf()
assertInternalType()
assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString()
assertLessThan()
assertLessThanOrEqual()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertStringMatchesFormat()
assertStringMatchesFormatFile()
assertSame()
assertSelectCount()
assertSelectEquals()
assertSelectRegExp()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertTag()
assertThat()
assertTrue()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
5. コマンドラインのテストランナー
Command-Line switches
6. フィクスチャ
tearDown() よりも setUp()
バリエーション
フィクスチャの共有
グローバルな状態
7. テストの構成
ファイルシステムを用いたテストスイートの構成
XML 設定ファイルを用いたテストスイートの構成
8. データベースのテスト
データベースのテストに対応しているベンダー
データベースのテストの難しさ
データベーステストの四段階
1. データベースのクリーンアップ
2. フィクスチャの準備
3–5. テストの実行、結果の検証、そして後始末
PHPUnit のデータベーステストケースの設定
getConnection() の実装
getDataSet() の実装
データベーススキーマ (DDL) とは?
ヒント: 自前でのデータベーステストケースの抽象化
データセットとデータテーブルについて知る
利用できる実装
外部キーには注意
自作のデータセットやデータテーブルの実装
接続 API
データベースアサーション API
テーブルの行数のアサーション
テーブルの状態のアサーション
クエリの結果のアサーション
複数のテーブルの状態のアサーション
よくある質問
PHPUnit は、テストごとにデータベーススキーマを作り直すの?
PDO を使ったアプリケーションじゃないと Database Extension を使えないの?
Too much Connections というエラーが出たらどうすればいい?
フラット XML や CSV のデータセットで NULL を扱う方法は?
9. 不完全なテスト・テストの省略
不完全なテスト
テストの省略
@requires によるテストのスキップ
10. テストダブル
スタブ
モックオブジェクト
ウェブサービスのスタブおよびモック
ファイルシステムのモック
11. テストの進め方
開発中のテスト
デバッグ中のテスト
12. テスト駆動開発
銀行口座の例
13. 振舞駆動開発
ボウリングゲームの例
14. コードカバレッジ解析
カバーするメソッドの指定
コードブロックの無視
ファイルのインクルードや除外
エッジケース
15. テストのその他の使用法
アジャイルな文書作成
複数チームでのテスト
16. 雛形ジェネレータ
テストケースクラスの雛形の作成
テストケースクラスからのクラスの雛形の作成
17. PHPUnit と Selenium
Selenium Server
インストール
PHPUnit_Extensions_Selenium2TestCase
PHPUnit_Extensions_SeleniumTestCase
18. ログ出力
テスト結果 (XML)
テスト結果 (TAP)
テスト結果 (JSON)
コードカバレッジ (XML)
コードカバレッジ (テキスト)
19. PHPUnit の拡張
PHPUnit_Framework_TestCase のサブクラスの作成
カスタムアサーションの作成
PHPUnit_Framework_TestListener の実装
PHPUnit_Extensions_TestDecorator のサブクラスの作成
PHPUnit_Framework_Test の実装
A. アサーション
B. アノテーション
@author
@backupGlobals
@backupStaticAttributes
@codeCoverageIgnore*
@covers
@coversNothing
@dataProvider
@depends
@expectedException
@expectedExceptionCode
@expectedExceptionMessage
@group
@outputBuffering
@preserveGlobalState
@requires
@runTestsInSeparateProcesses
@runInSeparateProcess
@test
@testdox
@ticket
C. XML 設定ファイル
PHPUnit
テストスイート
グループ
コードカバレッジ対象のファイルの追加や除外
ログ出力
テストリスナー
PHP INI 項目や定数、グローバル変数の設定
Selenium RC の設定ブラウザ
D. 目次
E. 参考文献
F. 著作権