PHPUnit ポケットガイド

ちょっとした調べものからより高度な機能まで

Bergmann Sebastian [FAMILY Given]

この作品は、Creative Commons Attribution License の下でライセンスされています。このライセンスの内容を確認するには、http://creativecommons.org/licenses/by/2.0/ を訪問するか、あるいは Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305, USA に手紙を送ってください。

PHPUnit 2.3 対応版: Updated on 2009-12-29.


序文
前提条件
本書はフリーです
本書で使用している規約
謝辞
1. 自動テスト
2. PHPUnit の目標
3. PHPUnit のインストール
4. コマンドラインのテストランナー
5. Fixtures
tearDown() よりも setUp()
バリエーション
スイートレベルの設定
6. 例外やパフォーマンス低下をテストする
例外
パフォーマンス低下
7. 不完全なテスト
8. テストファーストプログラミング
銀行口座の例
9. コードカバレッジ解析
10. スタブ
自己シャント
11. テストのその他の使用法
アジャイルな文書作成
複数チームでのテスト
デバッグのためのテスト
リファクタリング
12. PHPUnit と Phing
結果を整形する
13. PHPUnit の実装
14. PHPUnit API
概要
PHPUnit2_Framework_Assert
PHPUnit2_Framework_Test
PHPUnit2_Framework_TestCase
PHPUnit2_Framework_TestSuite
PHPUnit2_Framework_TestResult
パッケージの構成
15. PHPUnit の拡張
PHPUnit2_Framework_TestCase のサブクラスの作成
アサートクラスの作成
PHPUnit2_Extensions_TestDecorator のサブクラスの作成
PHPUnit2_Framework_Test を実装する
PHPUnit2_Framework_TestResult のサブクラスの作成
PHPUnit2_Framework_TestListener の実装
新しいテストランナーの作成
A. PHP 4 用の PHPUnit
B. 目次
C. 参考文献
D. 著作権

序文

「いつになったら PHPUnit のドキュメントを書くの?」この質問に対する私の答えは、 これまでずっとこうでした。「PHPUnit のドキュメントなんか必要ありません。 JUnit のドキュメントを読むなり JUnit の書籍を買うなどして、Java や JUnit のコードを PHP や PHPUnit に置き換えればいいだけじゃないですか。」 O'Reilly のドイツオフィスからやってきた Barbara Weiss と Alexandra Follenius にこの話をしたところ、もう一度考え直して PHPUnit についてのドキュメントを書くように勧められました。

前提条件

本書で扱う PHPUnit は、PHP 言語でテスト駆動開発を行うための オープンソースのフレームワークです。この版では PHPUnit のバージョン 2.3 を扱います。しかし、大半の例は PHPUnit バージョン 2.0 から 2.2 でも動作します。後半の「PHP 4 用の PHPUnit」では、 PHP 4 向けの旧バージョンの PHPUnit (現在は活発には開発されていません) を扱います。

本書を読むには、PHP 5 でのオブジェクト指向プログラミングを 十分理解している必要があります。ドイツ人のみなさんには、 [Bergmann2005] を PHP 5 でのオブジェクト指向プログラミングの入門としてお勧めします。 また、Andi Gutmans と Stig Bakken、Derick Rethans によって書かれたすばらしい英語の書籍 [GuBaRe2005] もあります。

本書はフリーです

本書は、Creative Commons ライセンスのもとで公開しています。 最新のバージョンは、常に http://www.phpunit.de/pocket_guide/ にあります。お望みなら、自由に配布したり内容を変更したりできます。 あなたが独自に変更したバージョンを公開されるのなら、 にフィードバックやパッチを送っていただければありがたいです。

本書で使用している規約

以下に、本書で使用している表記の規約を示します。

斜体

新しい用語・URL・email アドレス・ファイル名・拡張子・パス名・ ディレクトリ・Unix ユーティリティを表します。

等幅

コマンド・オプション・スイッチ・変数・関数・クラス・名前空間・ メソッド・モジュール・パラメータ・値・オブジェクト・ ファイルの内容・コマンドの出力結果を表します。

等幅の太字

ユーザが入力するコマンドやその他のテキストを表します。

等幅の斜体

ユーザが指定した値に置き換えられるテキストを表します。

本文と離れた位置に以下の形式で記述されている内容には、 十分注意してください。

注記

ちょっとしたコツやおすすめの方法、その他一般的な注意点です。 本文で扱っている内容についての有益な補足情報が含まれます。

警告

これは、警戒すべき点についての注意です。

謝辞

JUnit を作成した Kent Beck および Erich Gamma に感謝します。 JUnit に影響を受けたのが PHPUnit を作成するきっかけとなりました。 また、Kent Beck については "JUnit Pocket Guide" [Beck2004] を書いてくださったことにも感謝します。ここから影響を受け、 私は本書を書きました。O'Reilly で本書のスポンサーになっていただいている Allison Randal、Alexandra Follenius そして Barbara Weiss に感謝します。

PHP 5 の骨格となる Zend Engine 2 を開発された Andi Gutmans、Zeev Suraski そして Marcus Börger に感謝します。Xdebug を開発された Derick Rethans に感謝します。この PHP 拡張モジュールのおかげで、PHPUnit にコードカバレッジ機能をもたせることができました。最後に、Phing の PHPUnit タスクを書かれた Michiel Rook に感謝します。

第1章 自動テスト

どんなにすぐれたプログラマも、間違いを犯します。 よいプログラマとそうでないプログラマの違いは、 よいプログラマはテストを行って間違いをできるだけ早く発見してしまうことです。 テストをするのが早ければ早いほど間違いを発見しやすくなり、 またそれを修正しやすくなります。 リリース直前までテストを先延ばしにしておくことが非常に問題であるのはこのためです。 そんなことをすると、すべてのエラーを発見しきることができず、 発見したエラーを修正することも非常に難しくなります。結局は、 トリアージを行ってどのエラーに対応するかを判断しなければならなくなります。 なぜならすべてのエラーを完全に修正することは不可能だからです。

PHPUnit を使用したテストは、全体としてはあなたがこれまでに行ってきたことと同じです。 ただ、そのやり方が違うだけです。それは、テスト つまりあなたのプログラムが期待通りにふるまうことを調べることと 総合テスト つまり実行可能なコード片がソフトウェアの各部分 (部品) を自動的にテストすることとの違いになります。実行可能なコード片のことを、 単体テスト (unit test) と呼びます。

この章では、単純な print ベースのテストコードをもとにして完全な自動テストに書き換えていきます。 PHP 組み込みの array をテストするように頼まれたとしましょう。このオブジェクトの機能のひとつに、 関数 sizeof() があります。新しく作成された配列では、 sizeof() 関数は 0 を返すはずです。 そして要素を 1 つ追加すると sizeof()1 を返すようになるはずです。テストしたい内容を 例 1.1 に示します。

例 1.1: Array および sizeof() のテスト

<?php
$fixture = array();
// $fixture は空のはずです。
 
$fixture[] = 'element';
// $fixture はひとつの要素を含むはずです。
?>

期待通りの結果が得られているかどうかを調べるためのいちばん単純な方法は、 要素を追加する前と後に sizeof() の結果を表示することです (例 1.2 を参照ください)。 それぞれ 0 および 1 が得られたら、 array および sizeof() が期待通りに動作していることになります。

例 1.2: print を使用した Array および sizeof() のテスト

<?php
$fixture = array();
print sizeof($fixture) . "\n";
 
$fixture[] = 'element';
print sizeof($fixture) . "\n";
?>
0
1

このテストは、成功したかどうかの判断を (出力結果を見て) 手動で行わなければなりません。今度は、この判断を自動でできるようにしてみましょう。 例 1.3 では、 期待される結果と実際の結果をコード中で比較して、もしそれらの値が等しければ ok と表示します。もし not ok と表示された場合は、どこかがおかしいということがわかります。

例 1.3: 期待値と実際の値を比較することによる Array および sizeof() のテスト

<?php
$fixture = array();
print sizeof($fixture) == 0 ? "ok\n" : "not ok\n";
 
$fixture[] = "element";
print sizeof($fixture) == 1 ? "ok\n" : "not ok\n";
?>
ok
ok

今度は、相違があった際に例外を発生させる関数を用意して、 期待値と実際の値を比較する処理を抽出してみましょう (例 1.4)。 これには 2 つの利点があります。テストが記述しやすくなること、 そして何か問題があったときにのみそれを出力させることができるということです。

例 1.4: アサーション関数を使用した Array および sizeof() のテスト

<?php
$fixture = array();
assertTrue(sizeof($fixture) == 0);
 
$fixture[] = 'element';
assertTrue(sizeof($fixture) == 1);
 
function assertTrue($condition)
{
    if (!$condition) {
        throw new Exception('Assertion failed.');
    }
}
?>

これで、テストは完全に自動化されました。最初のバージョンでは単に テストする だけでしたが、このバージョンでは 自動テスト になっています。

自動テストを行う目的は、間違いを少なくすることです。 いくらすばらしいテストを行ったところで あなたのコードが完璧なものになるわけではありませんが、 自動テストを始めることで不具合の量を劇的に減らすことになるでしょう。 自動テストによってあなたのコードは信頼性の高いものとなり、大胆な設計変更 (リファクタリング) を行ったりチームメイトとの関係をよりよくしたり (複数チームでのテスト)、 その日の朝に比べて帰宅前のコードがよりよくなっていることを確信できたりといった効果があります。

第2章 PHPUnit の目標

いまのところ、組み込みの array および sizeof() 関数のテストしかありません。 PHP が提供する数多くの array_*() 関数をテストしようとすると、それらそれぞれについてテストを記述する必要があります。 それらのすべてのテストについて基盤部分を最初から書いていくこともできますが、 共通部分は一度だけ記述するようにし、 個々のテストではテスト固有の部分のみを記述していくほうがずっとよい方法です。 PHPUnit は、そのような基盤部分を提供します。

例 2.1 は、PHPUnit を使用する形式で 例 1.4 の 2 つのテストを書き直したものです。

例 2.1: PHPUnit を使用した Array および sizeof() のテスト

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
 
class ArrayTest extends PHPUnit2_Framework_TestCase {
    public function testNewArrayIsEmpty() {
        // 配列を作成します。
        $fixture = array();
 
        // 配列のサイズは 0 です。
        $this->assertEquals(0, sizeof($fixture));
    }
 
    public function testArrayContainsAnElement() {
        // 配列を作成します。
        $fixture = array();
 
        // 配列にひとつの要素を追加します。
        $fixture[] = 'Element';
 
        // 配列のサイズは 1 です。
        $this->assertEquals(1, sizeof($fixture));
    }
}
?>

例 2.1 では、 PHPUnit を使用してテストを記述する基本手順を説明しています。

  1. Class という名前のクラスのテストは、ClassTest という名前のクラスに記述します。

  2. ClassTest は、(ほとんどの場合) PHPUnit2_Framework_TestCase を継承します。

  3. テストは、引数をとらないパブリックメソッドとして test* という名前で記述します。

  4. テストメソッドの中で assertEquals() のようなアサーションメソッド (表 14.1 を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。

PHPUnit のようなフレームワークには、解決しなければならない制約があります。 その中のいくつかはお互いに相反するものです。 テストは、以下の条件を同時に満たす必要があります。

記述方法を習得しやすいこと。

テストの書き方を身に着けるのが難しければ、 開発者はそんなものを覚えようとしないでしょう。

記述しやすいこと。

テストが書きにくければ、開発者はそんなものを書こうとしないでしょう。

読みやすいこと。

テストコードには、外部からの要素を含めるべきではありません。 そうするとテストコードが周りのノイズに埋もれてしまいます。

実行しやすいこと。

ボタン一発でテストが実行でき、 その結果は明白な形式で表示されなければなりません。

すばやく実行できること。

一日に何百何千というテストを実行できるよう、 テストはすばやく実行できなければなりません。

独立していること。

テストは、お互い他のテストに影響を及ぼしてはいけません。 テストの実行順序を変えることでテストの結果が変わってしまってはいけません。

自由に組み合わせられること。

テストの数や組み合わせを自由に選択できなければなりません。 テストが独立している以上、これは当然のことです。

これらの制約の中には、相反する項目が 2 点あります。

習得のしやすさか書きやすさか。

一般に、テストにはプログラミング言語の全機能を必要としません。 多くのテストツールは、テストを記述するため必要最小限の機能のみを組み込んだ 独自のスクリプト言語を提供しています。その結果、テストは読みやすく、 また書きやすいものになります。 なぜならテストの内容から気をそらせるノイズがないためです。しかし、 またひとつ新たなプログラミング言語やプログラミングツールの使い方を覚える必要があり、 不便です。

独立か実行速度か。

ひとつのテストが他のテストの結果に何の影響もおよぼさないようにするには、 各テストの実行前に毎回テスト用の環境を構築し、 終了後には毎回それを元の状態に戻す必要があります。しかし、環境を構築する (例: データベースに接続し、特定の状態を表すデータを投入する) には長い時間がかかります。

PHPUnit は、テスト言語として PHP を使用することで これらの衝突を回避しようとしています。小規模で単純なテストを行う際には、 PHP の機能は行き過ぎた面もあるかもしれません。しかし、PHP を使用することで、 これまでの開発経験や開発ツールを武器として利用できます。 あまり気乗りしないテスターを納得させるため、 テストを最初に書き始める際の負担をできるだけ下げることが重要だと考えています。

PHPUnit では、実行速度よりもテストの独立性を重視しています。 独立したテストに価値があるのは、そのほうがより高品質のフィードバックが得られるからです。 一連のテストの最初のほうで失敗したことでその後のすべてのテストが失敗してしまい、 大量の失敗報告を受け取るようなことがなくなります。 このオリエンテーションでは独立したテストを目指し、 シンプルなオブジェクトを数多く作るという設計を心がけます。 各オブジェクトは独立してすばやくテストできます。結果としてよりよい設計 に加えてより高速なテストが可能となります。

PHPUnit ではほとんどのテストが成功することを想定しており、 成功したテストの詳細について報告することはあまり価値がないと考えています。 テストが失敗した場合には、そのことをしっかり報告しなければなりません。 大半のテストは成功すべきであり、 それらについては実行したテストの数以外に特にコメントすべき情報はありません。 これは、PHPUnit のコアではなく報告用のクラス群に組み込まれている機能です。 テストの結果が表示される際には実行したテストの数が表示されますが、 詳細が表示されるのは失敗したテストについてだけです。

テストは決め細やかに行うこと、つまりひとつのテストではひとつのオブジェクトの ひとつの機能についてをテストするようにすることが期待されています。 そのため、最初のテストは失敗し、テストの実行は終了し、 PHPUnit は失敗を報告します。多くの小さなテストを実行させることは一種の芸術です。 決め細やかなテストにより、システム全体の設計がよりよいものとなります。

PHPUnit でオブジェクトをテストする際には、 その公開インターフェースについてのみテストを行います。 公開されている振る舞いにのみ基づいてテストを行うことで、 設計上の困難な問題により早い段階で対応できるようになり、 設計ミスがシステムの大部分に影響を及ぼすことを避けられます。

第3章 PHPUnit のインストール

PHPUnit をインストールするには、 PEAR インストーラ を使用します。このインストーラは PEAR の根幹をなすものであり、 PHP のパッケージを配布する仕組みを提供しています。また、バージョン 4.3.0 以降のすべての PHP に同梱されています。

PHPUnit の配布に使用する PEAR チャネル (pear.phpunit.de) を、ローカルの PEAR 環境に登録する必要があります。

pear channel-discover pear.phpunit.de

これを行う必要があるのは最初の一度だけです。これで、PEAR インストーラは PHPUnit チャネルからパッケージをインストールできるようになります。

pear install phpunit/PHPUnit

注記

以前に pear.php.net から PHPUnitPHPUnit2 をインストールしている場合、まずそれをアンインストールしなければなりません。

インストールすると、PHPUnit のソースファイルがローカルの PEAR ディレクトリに格納されます。場所は、通常は /usr/lib/php/PHPUnit です。

PHPUnit がサポートしているのは PEAR インストーラを使用する方法のみですが、 PHPUnit を手動でインストールすることも可能です。そのためには、 以下の手順に従ってください。

  1. http://pear.phpunit.de/get/ からアーカイブをダウンロードし、それを php.ini 設定ファイルの include_path で指定したディレクトリに展開します。

  2. phpunit スクリプトを準備します。

    1. pear-phpunit スクリプトの名前を phpunit に変更します。

    2. その中の @php_bin@ という文字列を、 PHP コマンドラインインタプリタへのパス (通常は /usr/bin/php) に変更します。

    3. それを PATH の通ったディレクトリにコピーし、 実行可能属性を付与します (chmod +x phpunit)。

第4章 コマンドラインのテストランナー

phpunit コマンドを実行すると、PHPUnit のコマンドライン版テストランナーが起動します。 コマンドラインのテストランナーを使用したテストの様子を以下に示します。

phpunit ArrayTest
PHPUnit 2.3.0 by Sebastian Bergmann.

..

Time: 0.067288

OK (2 tests)

テストがひとつ実行されるたびに、PHPUnit コマンドラインツールはその経過を示す文字を出力します。

.

テストが成功した際に表示されます。

F

テストメソッドの実行中、アサーションに失敗した際に表示されます。

E

テストメソッドの実行中、エラーが発生した際に表示されます。

I

テストが「不完全」あるいは「未実装」とマークされている場合に表示されます (第 7 章 を参照ください)。

PHPUnit は、失敗 (failures)エラー (errors) を区別します。 「失敗」は PHPUnit のアサーションに違反した場合で、 「エラー」は予期せぬ例外や PHP のエラーが発生した場合となります。 この区別は、時に有用です。というのは、「エラー」は一般的に「失敗」 より修正しやすい傾向があるからです。 もし大量の問題が発生した場合は、まず「エラー」を最初に片付け、 その後で「失敗」を修正していくのが最良の方法です。

以下のコードで、コマンドライン版テストランナーのスイッチの一覧を見てみましょう。

phpunit --help
PHPUnit 2.3.0 by Sebastian Bergmann.

Usage: phpunit [switches] UnitTest [UnitTest.php]
  --coverage-data <file> Write Code Coverage data in raw format to file.
  --coverage-html <file> Write Code Coverage data in HTML format to file.
  --coverage-text <file> Write Code Coverage data 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.
  --log-xml <file>       Log test progress in XML format to file.

  --loader <loader>      TestSuiteLoader implementation to use.

  --skeleton             Generate skeleton UnitTest class for Unit in Unit.php.

  --wait                 Waits for a keystroke after each test.

  --help                 Prints this usage information.
  --version              Prints the version and exits.
phpunit UnitTest

UnitTest という名前のクラスで定義されている テストを実行します。このクラスは、UnitTest.php という名前のファイルの中に定義されているものとします。

UnitTest は、PHPUnit2_Framework_TestCase を継承したクラスであるか、あるいは PHPUnit2_Framework_Test オブジェクト、例えば PHPUnit2_Framework_TestSuite のインスタンスを返す public static suite() というメソッドを保持するクラスでなければなりません。

phpunit UnitTest UnitTest.php

UnitTest という名前のクラスで定義されているテストを実行します。 このクラスは、指定したファイルの中で定義されているものとします。

--coverage-data--coverage-html および --coverage-text

実行したテストについてのコードカバレッジ情報の収集・解析を制御します (第 9 章 を参照ください)。

--testdox-html および --testdox-text

実行したテストについて、HTML あるいはプレーンテキスト形式のドキュメントを生成します (第 11 章 を参照ください)。

--log-xml

テストの実行結果を XML 形式のログファイルに出力します。

以下に、ArrayTest のテストによって生成された XML ログファイルの例を示します。

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="ArrayTest" tests="2" failures="0" errors="0" time="0.020026">
    <testcase name="testNewArrayIsEmpty" class="ArrayTest" time="0.014449"/>
    <testcase name="testArrayContainsAnElement" class="ArrayTest" time="0.005577"/>
  </testsuite>
</testsuites>

この XML ログファイルは、テストケースクラス FailureErrorTesttestFailure および testError という 2 つのテストによって生成されたもので、 失敗とエラーが発生したことを示しています。

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="FailureErrorTest" tests="2" failures="1" errors="1" time="0.013603">
    <testcase name="testFailure" class="FailureErrorTest" time="0.011872">
      <failure message="" type="PHPUnit2_Framework_AssertionFailedError"></failure>
    </testcase>
    <testcase name="testError" class="FailureErrorTest" time="0.001731">
      <error message="" type="Exception"></error>
    </testcase>
  </testsuite>
</testsuites>
--loader

PHPUnit2_Runner_TestSuiteLoader を実装したクラスのうち、 実際に使用するものを指定します。

標準のテストスイートローダーは、現在の作業ディレクトリおよび PHP の設定項目 include_path で指定されているディレクトリからソースファイルを探します。 PEAR の命名規則に従い、Project_Package_Class クラスがソースファイル Project/Package/Class.php に対応します。

--skeleton

(UnitTest.php に記述された) Unit クラスに対して、テストケースクラス UnitTest の雛形を記述したファイル UnitTest.php を作成します。 作成されたテストケースクラスの中のテストケースは、不完全な状態です (第 7 章 を参照ください)。

以下に、Sample クラスをテストするクラスの雛形を作成する例を示します。

phpunit --skeleton Sample
PHPUnit 2.3.0 by Sebastian Bergmann.

Wrote test class skeleton for Sample to SampleTest.php.

phpunit SampleTest
PHPUnit 2.3.0 by Sebastian Bergmann.

I

Time: 0.007268
There was 1 incomplete test case:
1) testSampleMethod(SampleTest)


OK, but incomplete test cases!!!
Tests run: 1, incomplete test cases: 1.

既存のコードのテストを記述する際は、 以下のようなコードを何度となく繰り返し記述することになるでしょう。

public function testSampleMethod() {
}

PHPUnit は、既存のコードを解析して テストケースクラスの雛形にそれらを記述します。

--wait

各テストが終了するたびにキー入力待ちの状態になります。これは、 テストランナーが終了すると同時にウィンドウが閉じてしまうような場合に便利です。

注記

テストしようとしているコードに PHP の文法エラーがあった場合は、 コマンドラインのテストランナーは何もエラー情報を表示せずに終了します。 標準のテストスイートローダーは、テストスイート自体のソースファイルについては PHP の文法チェックをしますが、テストスイートが include しているその他のファイルについてはチェックしません。将来のバージョンでは、 PHP インタプリタのサンドボックス環境を使用することでこの問題を解決する予定です。

第5章 Fixtures

テストを記述する際にいちばん時間を食うのは、テストを開始するための事前設定と テスト終了後の後始末の処理を書くことです。この事前設定は、テストの fixture と呼ばれます。

例 2.1 では、fixture は $fixture という変数に格納された配列だけでした。 しかしたいていの場合は fixture はこれより複雑なものとなり、 それを準備するにはかなりの量のコードが必要です。本来のテストの内容が、 fixture を設定するためのコードの中に埋もれてしまうことになります。 この問題は、複数のテストで同じような fixture を設定する場合により顕著になります。 テストフレームワークの助けがなければ、 個々のテストのなかで同じような準備コードを繰り返し書くはめになってしまいます。

PHPUnit は、準備用のコードの共有をサポートしています。 各テストメソッドが実行される前に、setUp() という名前のテンプレートメソッドが実行されます。setUp() は、テスト対象のオブジェクトを生成するような処理に使用します。 テストメソッドの実行が終了すると、それが成功したか否かにかかわらず、 tearDown() という名前の別のテンプレートメソッドが実行されます。 tearDown() では、テスト対象のオブジェクトの後始末などを行います。

それでは、setUp() を使用してコードの重複を排除するように 例 2.1 を書き換えてみましょう。 まず最初にインスタンス変数 $fixture を宣言し、 メソッド内のローカル変数ではなくこちらを使用するようにします。 そして、array fixture の生成処理を setUp() メソッドに移動します。最後に、 テストメソッド内で重複しているコードを取り除き、 新しく作成したインスタンス変数を使用するようにします。つまり、 assertEquals() で使用しているローカル変数 $fixture を、$this->fixture に置き換えます。

例 5.1: setUp() を使用して Array fixture を作成する

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
 
class ArrayTest extends PHPUnit2_Framework_TestCase {
    protected $fixture;
 
    protected function setUp() {
        // Array fixture を作成します。
        $this->fixture = array();
    }
 
    public function testNewArrayIsEmpty() {
        // Array fixture のサイズは 0 のはずです。
        $this->assertEquals(0, sizeof($this->fixture));
    }
 
    public function testArrayContainsAnElement() {
        // Array fixture に要素を追加します。
        $this->fixture[] = 'Element';
 
        // Array fixture のサイズは 1 のはずです。
        $this->assertEquals(1, sizeof($this->fixture));
    }
}
?>

各テストメソッドが実行されるたびに、setUp() および tearDown() が一度ずつコールされます。 「テストケース内の全テストメソッドについて一度だけコールするように したほうがよいのではないか」とお考えになるかもしれませんが、 そのようにすると各テストを互いに独立した状態にすることが難しくなります。

テストメソッドごとに setUp() および tearDown() が一度ずつ実行されるだけでなく、 テストメソッドごとに、新しいテストケースクラスのインスタンスが作成されます (第 13 章 を参照ください)。

tearDown() よりも setUp()

setUp()tearDown() は理屈上では対称的になるはずですが、実際にはそうではありません。実際には、 tearDown() を実装する必要があるのは setUp() で外部リソース (ファイルやソケットなど) を割り当てた場合のみです。もし setUp() で単に PHP オブジェクトを作成しただけの場合は、 一般には tearDown() は必要ありません。しかし、もし setUp() で大量のオブジェクトを作成した場合には、 それらの後始末をするために tearDown() で変数を unset() したくなることもあるでしょう。 テストケースオブジェクト自体のガベージコレクションにはあまり意味がありません。

バリエーション

ふたつのテストがあって、それぞれの setup がほんの少しだけ違う場合にはどうなるでしょう? このような場合は、二種類の可能性が考えられます。

  • もし setUp() の違いがごくわずかなものなら、 その違う部分を setUp() からテストメソッドのほうに移動させます。

  • setUp() の違いが大きければ、 テストケースクラスを別に分ける必要があります。それぞれのクラスには、 setup の違いを表す名前をつけます。

スイートレベルの設定

PHPUnit には、スイートレベルでの設定をするための便利な方法はありません。 複数のテストの間で fixture を共有したいなんてことは、通常はめったにないはずです。 しかし、設計上の問題などでどうしても fixture を共有しなければならないこともあるでしょう。

複数のテスト間で共有する意味のある fixture の例として意味のあるものといえば、 データベースとの接続でしょう。テストのたびに新しいデータベース接続を毎回作成するのではなく、 最初にログインした状態を再利用するということです。こうすることで、 テストの実行時間を短縮できます。これを行うには、データベースに関するテストを DatabaseTests という名前のクラスに書き、デコレータ (decorator) オブジェクト TestSetup でテストスイートをラップします。オーバーライドした setUp() でデータベース接続をオープンし、 tearDown() で接続を閉じるようにします。この例を 例 5.2 に示します。DatabaseTestSetup デコレータを起動することで、 DatabaseTests のテストを行うことができます。例えば、 PHPUnit のコマンドライン版テストランナーでは phpunit DatabaseTestSetup とします。

例 5.2: スイートレベル設定のデコレータを書く

<?php
require_once 'PHPUnit2/Framework/TestSuite.php';
require_once 'PHPUnit2/Extensions/TestSetup.php';
 
class DatabaseTestSetup extends PHPUnit2_Extensions_TestSetup {
    protected $connection = NULL;
 
    protected function setUp() {
        $this->connection = new PDO(
          'mysql:host=wopr;dbname=test',
          'root',
          ''
        );
    }
 
    protected function tearDown() {
        $this->connection = NULL;
    }
 
    public static function suite() {
        return new DatabaseTestSetup(
          new PHPUnit2_Framework_TestSuite('DatabaseTests')
        );
    }
}
?>

このように fixture を共有することがテストの価値を下げてしまうということを、 まだうまく伝え切れていないかもしれません。問題なのは、 オブジェクト間の連携が密になってしまっているという設計なのです。 複数が連携しているようなテストを作って設計上の問題から目をそらしてしまうのではなく、 きちんと設計しなおした上で、スタブ (第 10 章 を参照ください) を使用するテストを書くことをお勧めします。

第6章 例外やパフォーマンス低下をテストする

テストクラスの基底クラスである PHPUnit2_Framework_TestCase を拡張するための方法を、PHPUnit では二通り提供しています。これにより、 例外やパフォーマンス低下のテストができるようになります。

例外

例外をテストするにはどうすればいいのでしょう? 例外が発生したかどうかを直接検出することはできないので、その代わりに PHP の例外処理機能を使用してテストを書きましょう。 例外をテストするための例を以下に示します。

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
 
class ExceptionTest extends PHPUnit2_Framework_TestCase {
    public function testException() {
        try {
            // ... 例外が発生するはずのコード ...
        }
 
        catch (Exception $expected) {
            return;
        }
 
        $this->fail('期待通りの例外が発生しませんでした。');
    }
}
?>

例外が発生するはずのコードで例外が発生しなかった場合、その後の fail() (表 14.2 を参照ください) のコールでテストが中断され、テストで問題が発生したことを通知します。 期待通りに例外が発生した場合は catch ブロックが実行され、テストは正常に終了します。

別の方法としては、PHPUnit2_Extensions_ExceptionTestCase を継承したテストクラスを作成し、 テストコードの内部で例外がスローされたかどうかを調べることもできます。 例 6.1 では、PHPUnit2_Extensions_ExceptionTestCase のサブクラスを作成し、テストしたい例外をその setExpectedException() メソッドに設定する方法を示します。期待した例外がスローされなかった場合は、 そのテストは失敗という扱いになります。

例 6.1: PHPUnit2_Extensions_ExceptionTestCase の使用法

<?php
require_once 'PHPUnit2/Extensions/ExceptionTestCase.php';
 
class ExceptionTest extends PHPUnit2_Extensions_ExceptionTestCase {
    public function testException() {
        $this->setExpectedException('Exception');
    }
}
?>
phpunit ExceptionTest
PHPUnit 2.3.0 by Sebastian Bergmann.

F

Time: 0.006798
There was 1 failure:
1) testException(ExceptionTest)
Expected exception Exception

FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0, Incomplete Tests: 0.

表 6.1 は、PHPUnit2_Extensions_ExceptionTestCase が実装している外部プロトコルをまとめたものです。

表6.1 ExceptionTestCase の外部プロトコル

メソッド意味
void setExpectedException(string $exceptionName)発生することを期待する例外の名前を $exceptionName に設定します。
String getExpectedException()発生することを期待する例外の名前を返します。

パフォーマンス低下

PHPUnit2_Extensions_PerformanceTestCase を継承したテストクラスを使用すると、 関数やメソッドの実行が制限時間内に終わったかどうかなどをテストすることができます。

PHPUnit2_Extensions_PerformanceTestCase のサブクラスを作成してその setMaxRunningTime() メソッドを使用し、実行時間の最大値を制限する方法を 例 6.2 で示します。 もしテストが制限時間内に終了しなければ、そのテストは失敗という扱いになります。

例 6.2: PHPUnit2_Extensions_PerformanceTestCase の使用法

<?php
require_once 'PHPUnit2/Extensions/PerformanceTestCase.php';
 
class PerformanceTest extends PHPUnit2_Extensions_PerformanceTestCase {
    public function testPerformance() {
        $this->setMaxRunningTime(2);
        sleep(1);
    }
}
?>

表 6.2 は、PHPUnit2_Extensions_PerformanceTestCase. が実装している外部プロトコルをまとめたものです。

表6.2 PerformanceTestCase の外部プロトコル

メソッド意味
void setMaxRunningTime(int $maxRunningTime)テストの所要時間の最大値を (秒単位で) $maxRunningTime に設定します。
integer getMaxRunningTime()このテストの最大所要時間を返します。

第7章 不完全なテスト

新しいテストケースクラスを作成する際には、 これから書くべきテストの内容をはっきりさせるために、 まず最初は以下のような空のテストメソッドを書きたくなることでしょう。

public function testSomething() {
}

しかし、PHPUnit フレームワークでは空のメソッドを「成功した」 と判断してしまうという問題があります。このような解釈ミスがあると、 テスト結果のレポートが無意味になってしまいます。 そのテストがほんとうに成功したのか、 それともまだテストが実装されていないのかが判断できないからです。 実装していないテストメソッドの中で $this->fail() をコールするようにしたところで事態は何も変わりません。 こうすると、テストが「失敗した」と判断されてしまいます。 これは未実装のテストが「成功」と判断されてしまうのと同じくらいまずいことです (訳注: レポートを見ても、そのテストがほんとうに失敗したのか、 まだ実装されていないだけなのかがわかりません)。

テストの成功を青信号、失敗を赤信号と考えるなら、 テストが未完成あるいは未実装であることを表すための黄信号が必要です。 そのような場合に使用するインターフェイスが PHPUnit2_Framework_IncompleteTest で、 これは未完成あるいは未実装のテストメソッドで発生する例外を表すものです。 このインターフェイスの標準的な実装が PHPUnit2_Framework_IncompleteTestError です。

例 7.1 では SampleTest というテストケースクラスを定義しています。 ここには testSomething() というテストメソッドが含まれています。 このメソッド内で例外 PHPUnit2_Framework_IncompleteTestError を発生させることで、このメソッドがまだ完成していないことをはっきりさせます。

例 7.1: テストに未完成の印をつける

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
require_once 'PHPUnit2/Framework/IncompleteTestError.php';
 
class SampleTest extends PHPUnit2_Framework_TestCase {
    public function testSomething() {
        // オプション: お望みなら、ここで何かのテストをしてください。
        $this->assertTrue(TRUE, 'これは既に動作します。');
 
        // ここで処理を止め、テストが未完成であるという印をつけます。
        // PHPUnit2_Framework_IncompleteTest インターフェイスを
        // 実装するクラスであれば、任意のクラスを使用できます。
        throw new PHPUnit2_Framework_IncompleteTestError(
          'このテストは、まだ実装されていません。'
        );
    }
}
?>

未完成のテストは、PHPUnit のコマンドライン版テストランナーでは以下のように I で表されます。

phpunit SampleTest
PHPUnit 2.3.0 by Sebastian Bergmann.

I

Time: 0.006657
There was 1 incomplete test case:
1) testSomething(SampleTest)
This test has not been implemented yet.

OK, but incomplete test cases!!!
Tests run: 1, incomplete test cases: 1.

第8章 テストファーストプログラミング

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

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

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

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

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

銀行口座の例

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

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

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

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

例 8.1: BankAccount クラスのテスト

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

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

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

<?php
class BankAccount {
    private $balance = 0;
 
    public function getBalance() {
        return $this->balance;
    }
}
?>

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

phpunit BankAccountTest
PHPUnit 2.3.0 by Sebastian Bergmann.

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

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

例 8.3: 完全な BankAccount クラス

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

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

phpunit BankAccountTest
PHPUnit 2.3.0 by Sebastian Bergmann.

....

Time: 0.057038

OK (4 tests)

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

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

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

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

第9章 コードカバレッジ解析

ユニットテストでコードをテストする方法はわかりました。でも、 テストそのものをテストするにはどうしたらいいのでしょう? テストされていないコードを見つけるには? 言い換えれば、まだテストで カバーされていない部分を見つけるには? 完全にテストができたことをどうやって確認するの? これらのすべての疑問に対する答えとなるのが、コードカバレッジ解析という手法です。 コードカバレッジ解析を行うと、 コードのどの部分がテストされたのかを調べることができるようになります。

PHPUnit のコードカバレッジ解析では、Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。 ステートメントカバレッジというのは、たとえば 100 行のコードで構成されるメソッドがあった場合に、 もしテストで実際に実行されたのがそのうちの 75 行だけだったなら、 そのメソッドのコードカバレッジは 75 パーセントだと考えるということです。

図 9.1 は、BankAccount クラス (例 8.3 を参照ください) のコードカバレッジレポートを、PHPUnit のコマンドライン版テストランナーで --coverage-html を指定することによって HTML 形式で出力したものです。実行可能な行は黒、 実行可能でない行はグレーで表示されており、実際に実行された行が強調表示されています。

図9.1 テストで完全にカバーできていない BankAccount クラス

テストで完全にカバーできていない BankAccount クラス

このコードカバレッジレポートからわかることは、テストを完璧にするには setBalance()depositMoney() および withdrawMoney() に正当な値を渡した場合のテストを追加する必要があるということです。 BankAccountTest クラスに追加すべきテストを 例 9.1 に示します。これによって、BankAccount クラスのテストケースを完全に網羅できるようになります。

例 9.1: テストで完全にカバーされている BankAccount クラス

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
require_once 'BankAccount.php';
 
class BankAccountTest extends PHPUnit2_Framework_TestCase {
    // ...
 
    public function testSetBalance() {
        $this->ba->setBalance(1);
        $this->assertEquals(1, $this->ba->getBalance());
    }
 
    public function testDepositAndWidthdrawMoney() {
        $this->ba->depositMoney(1);
        $this->assertEquals(1, $this->ba->getBalance());
 
        $this->ba->withdrawMoney(1);
        $this->assertEquals(0, $this->ba->getBalance());
    }
}
?>

図 9.2 を見ると、BankAccount クラスのコードが完全にテストされていることがわかります。

図9.2 完全にテストでカバーされた BankAccount クラス

完全にテストでカバーされた BankAccount クラス

Phing を使用してより詳細なコードカバレッジレポートを生成する方法を、 第 12 章 で説明します。

第10章 スタブ

いろいろな要因で失敗する可能性があるテストよりも、 単一の事項のみをテストするテストのほうが情報を得やすくなります。では、 できるだけ外部からの影響を受けないようなテストを作成するには どうすればいいのでしょうか? 単純なことです。 高価で面倒で頼りなくて遅くて複雑なリソースを、シンプルな PHP オブジェクトで作成したスタブに置き換えればいいのです。たとえば、 複雑な計算をする代わりに単に定数を返すだけのものを、テスト用に作成すればいいのです。

スタブを使用すると、高価な外部リソースを使用することによる問題を解決できます。 たとえばデータベース接続のようなリソースをテスト間で共有するには PHPUnit2_Extensions_TestSetup デコレータを使用すればいいのですが、 テストの目的を考えると、データベースを使用せずにすむのならばそのほうがずっとよいでしょう。

スタブを使用するもうひとつの利点は、よりよい設計になることです。 あちこちで使用されているリソースを単一の窓口 (façade : ファサード) 経由でアクセスするようにすることで、 それを簡単にスタブに置き換えられるようになります。例えば、 データベースへのアクセスのコードをそこらじゅうにちりばめるのではなく、 その代わりに IDatabase インターフェイスを実装した単一の Database オブジェクトを使用するようにします。すると、 IDatabase を実装したスタブを作成することで、 それをテストに使用できるようになるのです。同時に、 テストを行う際にスタブデータベースを使用するか 本物のデータベースを使用するかを選択できるようになります。 つまり開発時にはローカル環境でテストし、 統合テスト時には実際のデータベースでテストするといったことができるようになるのです。

スタブ化しなければならない機能は、たいてい同一オブジェクト内で密結合しています。 この機能ををひとつの結合したインターフェイスにまとめることで、 システムのそれ以外の部分との結合を緩やかにすることができます。

自己シャント

オブジェクトが正しくコールされたかどうかを確かめなければならないこともあるでしょう。 コールされるオブジェクトの完全なスタブを作ることはできますが、 正確な結果を返しているかどうかを確かめるのは不便です。もっと簡単な方法は、 自己シャント (Self Shunt) パターンを使用して、 テストケース自身をスタブとして使用することです。自己シャントというのは医学用語で、 薬を注入する場所を確保するため、 チューブを挿入して動脈からとった血液を静脈にもどすことを意味します。

ここに例を示します。別のオブジェクトを観察 (observe) しているオブジェクト上で、 正しいメソッドがコールされたかどうかをテストしたいとします。まず、 Observer を実装したテストケースクラスを作成します。

class ObserverTest extends PHPUnit2_Framework_TestCase implements Observer {
}

次に、Observer のメソッドである update() を実装します。そして、観察対象のオブジェクトである Subject の状態が変化した場合にそのメソッドが正しくコールされるかどうかを調べます。

public $wasCalled = FALSE;

public function update(Subject $subject) {
    $this->wasCalled = TRUE;
}

さあ、それではテストを書いてみましょう。まず新しい Subject オブジェクトを作成し、テストオブジェクトをオブザーバとしてアタッチします。 Subject の状態が変化すると (例えば doSomething() メソッドがコールされるなど)、Subject オブジェクトは全オブザーバの update() メソッドをコールしなければなりません。ここではインスタンス変数 $wasCalled を使用し、update() の中でその値を設定します。これにより、Subject オブジェクトが期待通りの動作をしているかどうかを確かめます。

public function testUpdate() {
    $subject = new Subject;
    $subject->attach($this);
    $subject->doSomething();

    $this->assertTrue($this->wasCalled);
}

グローバルなインスタンスではなく、新しい Subject オブジェクトを作成したことに注意してください。スタブを利用する際には、 このような設計をお勧めします。こうすると、 オブジェクト間の結合度が緩やかになり、再利用性が高まります。

自己シャントパターンになじみのない方には、このテストはわかりにくいかもしれません。 いったいここで何が起こっているんだ? テストケース自身がオブザーバになるだって? でも、お決まりのパターンをいったん身につけてしまえば、 これらのテストは簡単に理解できるようになります。 テストの内容をつかむために必要なことは、 すべてひとつのクラスの中に含まれているのですから。

第11章 テストのその他の使用法

自動テストに慣れてくると、 ほかの目的のためにもテストを使いたくなってくることでしょう。 ここではそんな例を説明します。

アジャイルな文書作成

一般的に、エクストリームプログラミングのようなアジャイルプロセスを採用しているプロジェクトでは、 ドキュメントの内容が実際の設計やコードに追いついていないことが多いものです。 エクストリームプログラミングでは コードの共同所有 (collective code ownership) を要求しており、 すべての開発者がシステム全体の動作を知っておく必要があります。 作成するテストに対して、そのクラスが何を行うべきなのかを示すような 「わかりやすい」名前をつけられるようにさえしておけば、PHPUnit の TestDox 機能を使用して自動的にドキュメントを生成することができます。 このドキュメントにより、開発者たちはプロジェクト内の各クラスが どのようにふるまうべきなのかを知ることができます。

PHPUnit の TestDox 機能は、テストクラス内のすべてのテストメソッドの名前を抽出し、 それを PHP 風のキャメルケースから通常の文に変換します。つまり testBalanceIsInitiallyZero() が "Balance is initially zero" のようになるわけです。最後のほうの数字のみが違うメソッド、例えば testBalanceCannotBecomeNegative()testBalanceCannotBecomeNegative2() のようなものが存在した場合は、 文 "Balance cannot become negative" は一度のみ表示され、 全てのテストが成功したことを表します。

以下のコードは、phpunit --testdox-text BankAccountTest.txt BankAccountTest を実行させて出力した、BankAccount クラス (例 8.1 を参照ください) のアジャイルな文書の例です。

BankAccount
 - Balance is initially zero
 - Balance cannot become negative

また、--testdox-html BankAccountTest.htm を使用することで、アジャイルな文書を HTML 形式で作成することもできます。

アジャイルな文書は、プロジェクト内であなたが作成しようとしている外部パッケージについて、 このように動作するであるという期待をまとめた文書にもなります。 外部のパッケージを使用するときには、 そのパッケージが期待通りに動作しなくなるというリスクに常にさらされています。 パッケージのバージョンアップにより知らないうちに挙動が変わってしまい、 あなたのコードが動作しなくなる可能性もあります。そのようなことを避けるため、 「このパッケージはこのように動作するはず」 ということを常にテストケースで記述しておくようにします。テストが成功すれば、 期待通りに動作していることがわかります。もし動作仕様をすべてテストで記述できているのなら、 外部パッケージが将来バージョンアップされたとしても何の心配もいりません。 テストをクリアしたということは、システムは期待通りに動作するということだからです。

複数チームでのテスト

あるパッケージについての機能を文書化するためにテストを書いているとき、 そのテストの所有者はあなたです。今あなたがテストを作成しているパッケージの作者は、 そのテストのことについては何も知りません。パッケージの作者とよりつながりを深めるため、 作成したテストを使用してコミュニケートしたり、 そのテストを使用して共同作業をしたりすることができるでしょう。

あなたが作成したテストを使用してパッケージの作者と共同作業をすることになれば、 テストも共同で書くことになります。そうすることで、 より多くのテストケースを挙げられるようになるでしょう。 「暗黙の了解」などに頼っていては、共同作業はできません。 テストと同時に、あなたはそのパッケージに対して期待していることを正確に文書化することになります。 また、すべてのテストにクリアした時点で、 作者はパッケージが完成したことを知ることになります。

スタブ (本書の前のほうで説明した "スタブ" の章を参照ください) を使用することで、パッケージの作者と別れても作業できるようになります。 パッケージ作者の仕事は、パッケージの実際の実装でテストをクリアするようにすること。 そしてあなたの仕事はあなたが書いたコードでテストをクリアするようにすることです。 この段階になれば、あなたはスタブオブジェクトを使用すればよいのです。 このやり方により、2 つのチームが独立して開発できるようになります。

デバッグのためのテスト

不具合の報告を受けたら、すぐにでもそれを修正したいと思われることでしょう。 しかし、あせって修正しようとしても、経験上なかなかうまくいきません。 不具合を修正したつもりが新たな不具合を引き起こしていたなんてこともありがちですね。

はやる気持ちを抑えて、以下のようにしてみましょう。

  1. 不具合を再現できるかどうかを確認します。

  2. 不具合が発生する最小限のコードを見つけます。例えば、 もしおかしな数値が出力されるのなら、 その数値を計算しているオブジェクトが何なのかを探します。

  3. その不具合のせいで今は失敗する (そして、不具合が修正されたら成功する) テストを書きます。

  4. 不具合を修正します。

不具合が再現する最小限のコードを見つける過程で、 不具合の原因がわかるかもしれません。テストを書くことによって、 不具合を真の意味で修正できる可能性が高まるでしょう。なぜなら、 テストを書くことで、将来同じ間違いをする可能性を減らせるからです。 これまでに書いたすべてのテストが、 不注意によって別の問題を発生させる可能性を減らすために役立っているのです。

リファクタリング

リファクタリングとは既存のコードの設計をよりよくするためのテクニックですが、 これを安全に行えるのは、きちんとしたテストスイートを持っている場合のみです。 さもないと、リファクタリングによってシステムを壊してしまっても あなたはそれに気づかないでしょう。リファクタリングは、 振る舞いを保持したままの細かい変更の繰り返しと考えることができます。

以下の条件が、あなたのプロジェクトのコードや設計を改善するための助けとなるでしょう。 また、単体テストを使用することで、リファクタリングによって振る舞いが変化していないこと・ エラーが発生していないことが確認できます。

  1. すべての単体テストが正常に動作すること。

  2. コードが設計指針を満たしていること。

  3. コードに冗長性がないこと。

  4. コードには最小限のクラスおよびメソッドのみが含まれていること。

第12章 PHPUnit と Phing

Phing (PHing Is Not GNU make) は、Apache Ant をもとにしてつくられたプロジェクトビルドシステムです。 PHP の世界ではソースファイルをビルドしたりコンパイルしたりする必要はありません。 Phing が意図しているのは、パッケージの作成や配布、 そしてアプリケーションのテストを容易にすることです。 これらの作業を行うために、Phing には多くの独創的な操作モジュール ("タスク") が用意されており、 また独自のタスクを追加しやすいようにオブジェクト指向のモデルが提供されています。

Phing は PEAR インストーラでインストールできます。 コマンドラインで以下のように入力してください。

pear channel-discover pear.phing.info
pear install phing/phing

Phing ではシンプルな XML 形式のビルドファイルを使用しており、 そこで対象のツリーや実行されるタスクを指定します。 Phing に含まれる独創的なタスクのひとつに <phpunit2> タスクというものがあり、 これを使用すると PHPUnit フレームワークでテストケースを実行します。 これは、Apache Ant における JUnit タスクと同じ機能を提供するものです。

例 12.1 で示しているのは、"BankAccount" という名前の <project> を指定した build.xml ファイルです。 このプロジェクトのデフォルトの <target>test となっています。<phpunit2> タスクを使用すると、*Test.php に一致するソースファイル内のすべてのテストケースを実行します。 これは <batchtest> 要素によって実現されており、 この要素は、内部の <fileset> 要素に含まれるすべてのファイルをとりまとめます。この例では、ソースファイル BankAccountTest.php に含まれるクラス BankAccountTest で宣言されているテストが実行されるわけです。

例 12.1: BankAccount のテスト用の Phing build.xml ファイル

<?xml version="1.0"?>

<project name="BankAccount" basedir="." default="test">
  <target name="test">
    <phpunit2 haltonfailure="true" printsummary="true">
      <batchtest>
        <fileset dir=".">
          <include name="*Test.php"/>
        </fileset>
      </batchtest>
    </phpunit2>
  </target>
</project>

build.xml (例 12.1)、 BankAccount.php (例 8.3)、 そして BankAccountTest.php (例 8.1) が存在するディレクトリで Phing を起動すると、 プロジェクトのデフォルトのターゲットである tests にしたがってテストが行われます。

phing
Buildfile: /home/sb/build.xml

BankAccount > test:
 [phpunit2] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 0.00067 sec

BUILD FINISHED

Total time: 0.0960 seconds

<phpunit2> タスクを設定する際に使用できる パラメータの一覧を 表 12.1 にまとめます。

表12.1 <phpunit2> 要素の属性

名前説明デフォルト
codecoverageBooleanコードカバレッジ情報を収集します。false
haltonerrorBooleanテストの実行中にエラーが発生したら、ビルドプロセスを停止します。false
haltonfailureBooleanテストが失敗したら、ビルドプロセスを停止します。エラーも失敗と同様に扱います。false
printsummaryBoolean各テストケースについて、状態を一行で出力します。false

以下は、テストが失敗した場合の <phpunit2> タスクの出力例です。

phing
Buildfile: /home/sb/build.xml

BankAccount > test:
 [phpunit2] Tests run: 4, Failures: 1, Errors: 0, Time elapsed: 0.00067 sec
Execution of target "test" failed for the following reason:
/home/sb/build.xml:5:37: One or more tests failed

BUILD FAILED
/home/sb/build.xml:5:37: One or more tests failed
Total time: 0.0968 seconds

結果を整形する

必須要素である <batchtest> 以外にも、 <phpunit2> には別の要素を含めることができます。 <formatter> はテスト結果の出力書式を変更するために使用するものです。 usefile 属性を false にしない限り、 出力は常にファイルに送信されます。 ファイル名はフォーマッタによって事前に決められていますが、 outfile 属性を使用して変更することも可能です。 定義済みのフォーマッタには以下のようなものがあります。

brief

失敗したテストケースについての情報のみをプレーンテキストで表示します。

plain

すべてのテストケースについて、結果を一行のプレーンテキストで表示します。

xml

テストの結果を XML 形式で書き込みます。

<formatter> タスクを設定するために使用できるパラメータの一覧を 表 12.2 にまとめます。

表12.2 <formatter> 要素の属性

名前説明デフォルト
typeString定義済みのフォーマッタ (xml、plain あるいは brief) を使用します。 
classnameString独自のフォーマッタクラスの名前。 
usefileBoolean出力をファイルに送るかどうかを指定します。true
todirStringファイルを書き込むディレクトリ。 
outfileString書き込むファイル名。使用するフォーマッタに依存します。

テスト結果を HTML 形式で出力するには <phpunit2report> タスクを使用します。これは、<formatter> タスクで作成した XML ログファイルに XSLT スタイルシートを適用するものです。 Phing には phpunit2-frames.xslphpunit2-noframes.xsl の 2 種類の XSLT スタイルシートが含まれており、それぞれフレーム使用版/フレーム未使用版の HTML を生成します。

例 12.2 では、Phing で BankAccountTest クラスのテストを実行し、 XSLT スタイルシート phpunit2-frames.xsl を使用して結果を HTML 形式で得るための build.xml ファイルを示します。レポートが生成した HTML ファイルは report/ ディレクトリに保存されます。このディレクトリは prepare <target> で作成され、clean <target> で削除されます。

例 12.2: XSLT スタイルシートを適用してテストレポートを取得する

<?xml version="1.0"?>

<project name="BankAccount" basedir="." default="report">
  <target name="prepare">
    <mkdir dir="report"/>
  </target>

  <target name="clean"> 
    <delete dir="report"/>
  </target>

  <target name="report" depends="prepare">
    <phpunit2>
      <batchtest>
        <fileset dir=".">
          <include name="*Test.php"/>
        </fileset>
      </batchtest>

      <formatter type="xml" todir="report" outfile="logfile.xml"/>
    </phpunit2>

    <phpunit2report infile="report/logfile.xml"
                    styledir="/usr/lib/php/data/phing/etc"
                    format="frames"
                    todir="report"/>
  </target>
</project>

以下に、phing コマンドを実行した際の出力例を示します。

phing
Buildfile: /home/sb/build.xml

BankAccount > prepare:
    [mkdir] Created dir: /home/sb/report

BankAccount > report:

BUILD FINISHED

Total time: 0.1112 seconds

生成されたテストレポートのタイトルページは 図 12.1 のようになります。

図12.1 生成されたテストレポート

生成されたテストレポート

<phpunit2report> タスクを設定するために使用できるパラメータの一覧を 表 12.3 にまとめます。

表12.3 <phpunit2report> 要素の属性

名前説明デフォルト
infileString使用する XML 結果ファイル名。testsuites.xml
formatString生成される結果の書式。frames あるいは noframes のいずれかでなければなりません。noframes
styledirStringスタイルシートの存在する場所。スタイルシートは、以下の規約を満たすものでなければなりません。frames フォーマット用のスタイルシートの名前は phpunit2-frames.xsl であること。また、noframes フォーマット用のスタイルシートの名前は phpunit2-noframes.xsl であること。 
todirString変換後のファイルを書き込むディレクトリ。 

いま作成したテストレポートのほかに、 Phing ではコードカバレッジレポートを作成することもできます。そのためには、 <coverage-setup> タスクおよび <coverage-report> タスクを使用します。 前者はテストの実行中にコードカバレッジ情報を保存するためのデータベースを準備するタスクで、 後者は XSLT スタイルシートを使用して結果を HTML 形式に変換するタスクです。

例 12.3 では、 Phing で BankAccountTest クラスのテストを実行し、 HTML 形式のコードカバレッジレポートを生成するための build.xml ファイルを示します。

例 12.3: コードカバレッジレポートの生成

<?xml version="1.0"?>

<project name="BankAccount" basedir="." default="coverage-report">
  <target name="prepare">
    <mkdir dir="coverage-report"/>
  </target>

  <target name="clean"> 
    <delete dir="coverage-report"/>
  </target>

  <target name="coverage-report" depends="prepare">
    <coverage-setup database="./coverage-report/database">
      <fileset dir=".">
        <include name="*.php"/>
        <exclude name="*Test.php"/>
      </fileset>
    </coverage-setup>

    <phpunit2 codecoverage="true">
      <batchtest>
        <fileset dir=".">
          <include name="*Test.php"/>
        </fileset>
      </batchtest>
    </phpunit2>

    <coverage-report outfile="coverage-report/coverage.xml">
      <report styledir="/usr/lib/php/data/phing/etc"
              todir="coverage-report"/>
    </coverage-report>
  </target>
</project>

生成されたコードカバレッジレポートのタイトルページは 図 12.2 のようになります。

図12.2 生成されたコードカバレッジレポート

生成されたコードカバレッジレポート

第13章 PHPUnit の実装

PHPUnit の実装はちょっと見慣れないものでしょう。 通常のアプリケーションでは保守しづらくなるようなテクニックを使用したりしています。 PHPUnit がテストを実行する仕組みを知っておくと、 あなたがテストを書く際に役立つこともあるでしょう。

個々のテストは PHPUnit2_Framework_Test のオブジェクトで表され、テストを実行するには PHPUnit2_Framework_TestResult のオブジェクトが必要です。PHPUnit2_Framework_TestResult オブジェクトが PHPUnit2_Framework_Test オブジェクトの run() メソッドに渡され、 このメソッドが実際のテストメソッドを実行します。そこで発生した例外を PHPUnit2_Framework_TestResult オブジェクトに報告します。 これは、Smalltalk の世界では Collecting Parameter と呼ばれているお決まりのパターンです。複数のメソッドの結果 (ここでは、各テストを起動する run() メソッドの結果) を一箇所にまとめたい場合は、メソッドにパラメータを追加すればそれが結果を集めてくれるのです。 Erich Gamma と Kent Beck の "JUnit: A Cook's Tour" [GammaBeck1999] や Kent Beck の "Smalltalk Best Practice Patterns" [Beck1997] [Beck1997-ja] を参照ください。

PHPUnit がテストを実行するしくみをより深く探るため、 例 13.1 のようなテストクラスを考えてみましょう。

例 13.1: EmptyTest クラス

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
 
class EmptyTest extends PHPUnit2_Framework_TestCase {
    private $emptyArray = array();
 
    public function testSize() {
        $this->assertEquals(0, sizeof($this->emptyArray));
    }
 
    public function testIsEmpty() {
        $this->assertTrue(empty($this->emptyArray));
    }
}
?>

テストが実行されるときに PHPUnit がまず行うのは、テストクラスを PHPUnit2_Framework_Test オブジェクトに変換することです。 ここでは、PHPUnit2_Framework_TestSuite には 図 13.1 に見られるように 2 つの EmptyTest インスタンスが含まれます。

図13.1 実行しようとしているテスト

実行しようとしているテスト

PHPUnit2_Framework_TestSuite の実行時には、各 EmptyTest が順に実行されます。その中では各自の setUp() メソッドが実行され、各テストについて 図 13.2 に見られるような新しい $emptyArray を作成します。 こうすることで、あるテストが配列を変更したとしても それが他のテストに影響を及ぼさないようになります。 仮にグローバル変数やスーパーグローバル変数 ($_ENV など) を変更したとしても、それは他のテストには影響を及ぼしません。

図13.2 テストを実行した後の fixture の状態

テストを実行した後の fixture の状態

つまり、テストが実行される際には、ひとつのテストケースクラスが 2 段階のオブジェクトツリーになるということです。各テストは setUp() で作成された自分自身のコピーの上で実行され、テストは完全に独立して実行されます。

PHPUnit は、リフレクションを使用してインスタンス変数 $name からメソッド名を取得し、そのテストメソッドを実行します。これは、Smalltalk の世界では Pluggable Selector と呼ばれているお決まりのパターンです。 Pluggable Selector を使用することでテストをよりシンプルに書くことができますが、 その代わりコードを見ただけではどのメソッドが実行されるのかがわからなくなります。 実行されるメソッドを知るには、実行時のデータの値を調べなければならないのです。

第14章 PHPUnit API

たいていの場合は、PHPUnit の API は単純なものです。単に PHPUnit2_Framework_TestCase を継承したテストケースを作成し、 assertTrue() あるいは assertEquals() をコールすればよいのです。しかし、PHPUnit をより深く知りたい方のために、 ここではすべてのクラスおよび公開メソッドを説明します。

概要

ほとんどの場合、PHPUnit を使用する際には以下の 5 つのクラスやインターフェイスに出会うことになるでしょう。

PHPUnit2_Framework_Assert

実際の値が想定した値どおりかどうかを調べるための静的メソッドを集めたもの。

PHPUnit2_Framework_Test

テストケースとして動作するすべてのオブジェクトのインターフェイス。

PHPUnit2_Framework_TestCase

ひとつのテスト。

PHPUnit2_Framework_TestSuite

テストの集まり。

PHPUnit2_Framework_TestResult

ひとつあるいは複数のテストの実行結果をまとめたもの。

PHPUnit の、5 つの基本クラス/インターフェイスである PHPUnit2_Framework_AssertPHPUnit2_Framework_TestPHPUnit2_Framework_TestCasePHPUnit2_Framework_TestSuite および PHPUnit2_Framework_TestResult の関係を 図 14.1 に示します。

図14.1 PHPUnit の 5 つの基本クラス/インターフェイス

PHPUnit の 5 つの基本クラス/インターフェイス

PHPUnit2_Framework_Assert

PHPUnit 用に書かれたテストケースのほとんどは、間接的に PHPUnit2_Framework_Assert を継承しています。ここには、 値を自動的にチェックして矛盾を報告するためのメソッドが含まれています。 これらのメソッドは静的に宣言されているので、 あなたが作成したメソッドの中で「規約による設計」方式のアサーションを使用し、 PHPUnit に結果を報告させることができます (例 14.1 を参照ください)。

例 14.1: 「規約による設計」方式のアサーション

<?php
require_once 'PHPUnit2/Framework/Assert.php';
 
class Sample {
    public function aSampleMethod($object) {
        PHPUnit2_Framework_Assert::assertNotNull($object);
    }
}
 
$sample = new Sample;
$sample->aSampleMethod(NULL);
?>
Fatal error: Uncaught exception 'PHPUnit2_Framework_AssertionFailedError'
with message 'expected: <NOT NULL> but was: <NULL>'

しかし、ほとんどの場合はこれらのアサーションはテストの中で行います。

各アサーションメソッドには 2 種類の方式があります。 エラー時に表示されるメッセージをパラメータとして指定する方法としない方法です。 オプションで指定したメッセージは、通常はテストが失敗したことが報告される場面で 表示されます。これにより、デバッグが楽になります。

例 14.2: メッセージつきのアサーション

<?php
require_once 'PHPUnit2/Framework/TestCase.php';
 
class MessageTest extends PHPUnit2_Framework_TestCase {
    public function testMessage() {
        $this->assertTrue(FALSE, 'これは独自のメッセージです。');
    }
}
?>

以下の例は、 例 14.2 のテスト testMessage() でメッセージつきのアサーションを使用した場合の出力結果です。

phpunit MessageTest.php
PHPUnit 2.3.0 by Sebastian Bergmann.

F

Time: 0.102507
There was 1 failure:
1) testMessage(MessageTest)
これは独自のメッセージです。

FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0, Incomplete Tests: 0.

表 14.1 に、すべてのアサーションをまとめます。

表14.1 アサーション

アサーション意味
void assertTrue(bool $condition)$conditionFALSE の場合にエラーを報告します。
void assertTrue(bool $condition, string $message)$conditionFALSE の場合にエラー $message を報告します。
void assertFalse(bool $condition)$conditionTRUE の場合にエラーを報告します。
void assertFalse(bool $condition, string $message)$conditionTRUE の場合にエラー $message を報告します。
void assertNull(mixed $variable)$variableNULL でないときにエラーを報告します。
void assertNull(mixed $variable, string $message)$variableNULL でないときにエラー $message を報告します。
void assertNotNull(mixed $variable)$variableNULL の場合にエラーを報告します。
void assertNotNull(mixed $variable, string $message)$variableNULL の場合にエラー $message を報告します。
void assertSame(object $expected, object $actual)2 つの変数 $expected$actual が同じオブジェクトを参照していない場合にエラーを報告します。
void assertSame(object $expected, object $actual, string $message)2 つの変数 $expected$actual が同じオブジェクトを参照していない場合にエラー $message を報告します。
void assertSame(mixed $expected, mixed $actual)2 つの変数 $expected$actual が同じ型・同じ値でない場合にエラーを報告します。
void assertSame(mixed $expected, mixed $actual, string $message)2 つの変数 $expected$actual が同じ型・同じ値でない場合にエラー $message を報告します。
void assertNotSame(object $expected, object $actual)2 つの変数 $expected$actual が同じオブジェクトを参照している場合にエラーを報告します。
void assertNotSame(object $expected, object $actual, string $message)2 つの変数 $expected$actual が同じオブジェクトを参照している場合にエラー $message を報告します。
void assertNotSame(mixed $expected, mixed $actual)2 つの変数 $expected$actual が同じ型・同じ値である場合にエラーを報告します。
void assertNotSame(mixed $expected, mixed $actual, string $message)2 つの変数 $expected$actual が同じ型・同じ値である場合にエラー $message を報告します。
void assertEquals(array $expected, array $actual)2 つの配列 $expected$actual が等しくない場合にエラーを報告します。
void assertEquals(array $expected, array $actual, string $message)2 つの配列 $expected$actual が等しくない場合にエラー $message を報告します。
void assertNotEquals(array $expected, array $actual)2 つの配列 $expected$actual が等しい場合にエラーを報告します。
void assertNotEquals(array $expected, array $actual, string $message)2 つの配列 $expected$actual が等しい場合にエラー $message を報告します。
void assertEquals(float $expected, float $actual, '', float $delta = 0)2 つの float 値 $expected$actual の誤差が $delta より大きい場合にエラーを報告します。
void assertEquals(float $expected, float $actual, string $message, float $delta = 0)2 つの float 値 $expected$actual の誤差が $delta より大きい場合にエラー $message を報告します。
void assertNotEquals(float $expected, float $actual, '', float $delta = 0)2 つの float 値 $expected$actual の誤差が $delta 以下のにエラーを報告します。
void assertNotEquals(float $expected, float $actual, string $message, float $delta = 0)2 つの float 値 $expected$actual の誤差が $delta 以下の場合にエラー $message を報告します。
void assertEquals(string $expected, string $actual)2 つの文字列 $expected$actual が等しくない場合にエラーを報告します。エラーは、2 つの文字列の差分で報告されます。
void assertEquals(string $expected, string $actual, string $message)2 つの文字列 $expected$actual が等しくない場合にエラー $message を報告します。エラーは、2 つの文字列の差分で報告されます。
void assertNotEquals(string $expected, string $actual)2 つの文字列 $expected$actual が等しい場合にエラーを報告します。
void assertNotEquals(string $expected, string $actual, string $message)2 つの文字列 $expected$actual が等しい場合にエラー $message を報告します。
void assertEquals(mixed $expected, mixed $actual)2 つの変数 $expected$actual が等しくない場合にエラーを報告します。
void assertEquals(mixed $expected, mixed $actual, string $message)2 つの変数 $expected$actual が等しくない場合にエラー $message を報告します。
void assertNotEquals(mixed $expected, mixed $actual)2 つの変数 $expected$actual が等しい場合にエラーを報告します。
void assertNotEquals(mixed $expected, mixed $actual, string $message)2 つの変数 $expected$actual が等しい場合にエラー $message を報告します。
void assertContains(mixed $needle, array $haystack)$needle$haystack の要素でない場合にエラーを報告します。
void assertContains(mixed $needle, array $haystack, string $message)$needle$haystack の要素でない場合にエラー $message を報告します。
void assertNotContains(mixed $needle, array $haystack)$needle$haystack の要素である場合にエラーを報告します。
void assertNotContains(mixed $needle, array $haystack, string $message)$needle$haystack の要素である場合にエラー $message を報告します。
void assertContains(mixed $needle, Iterator $haystack)$needle$haystack の要素でない場合にエラーを報告します。
void assertContains(mixed $needle, Iterator $haystack, string $message)$needle$haystack の要素でない場合にエラー $message を報告します。
void assertNotContains(mixed $needle, Iterator $haystack)$needle$haystack の要素である場合にエラーを報告します。
void assertNotContains(mixed $needle, Iterator $haystack, string $message)$needle$haystack の要素である場合にエラー $message を報告します。
void assertRegExp(string $pattern, string $string)$string が正規表現 $pattern にマッチしない場合にエラーを報告します。
void assertRegExp(string $pattern, string $string, string $message)$string が正規表現 $pattern にマッチしない場合にエラー $message を報告します。
void assertNotRegExp(string $pattern, string $string)$string が正規表現 $pattern にマッチする場合にエラーを報告します。
void assertNotRegExp(string $pattern, string $string, string $message)$string が正規表現 $pattern にマッチする場合にエラー $message を報告します。
void assertType(string $expected, mixed $actual)変数 $actual の型が $expected でない場合にエラーを報告します。
void assertType(string $expected, mixed $actual, string $message)変数 $actual の型が $expected でない場合にエラー $messageを報告します。
void assertNotType(string $expected, mixed $actual)変数 $actual の型が $expected である場合にエラーを報告します。
void assertNotType(string $expected, mixed $actual, string $message)変数 $actual の型が $expected である場合にエラー $messageを報告します。

これら以外に、プロジェクトで使用している オブジェクト固有のアサーションが必要になることもあるでしょう。独自の Assert クラスを作成し、 そこに独自のアサーションを含めてテストに使用することができます。

アサーションに失敗すると、ボトルネックメソッド fail(string $message) がコールされ、これは PHPUnit2_Framework_AssertionFailedError をスローします。 このメソッドにもパラメータなしのものがあります。テストでエラーが発生した際に、 fail() を明示的にコールします。 例外が発生することが期待されるテストなどがその例になります。 表 14.2 に、PHPUnit のボトルネックメソッドをまとめます。

表14.2 ボトルネックメソッド

メソッド意味
void fail()エラーを報告します。
void fail(string $message)エラー $message を報告します。

PHPUnit2_Framework_Test

PHPUnit2_Framework_Test は、 テストとして働くすべてのオブジェクトが使用する、 一般的なインターフェイスです。これを実装したオブジェクトは、 ひとつあるいは複数のテストを表すことになります。 表 14.3 に示す 2 つのメソッドが定義されています。

表14.3 実装することになるメソッド

メソッド意味
int countTestCases()テストの数を返します。
void run(PHPUnit2_Framework_TestResult $result)テストを実行し、結果を $result で報告します。

PHPUnit2_Framework_Test の実装クラスとして有名なのは、 PHPUnit2_Framework_TestCase および PHPUnit2_Framework_TestSuite の 2 つです。 PHPUnit2_Framework_Test を実装したクラスを独自に作成することも可能です。 このインターフェイスはあえて小規模に設計されているので、実装するのは簡単でしょう。

PHPUnit2_Framework_TestCase

テストケースクラスは PHPUnit2_Framework_TestCase クラスを継承して作成します。たいていの場合は、 テストスイートから自動的にテストを実行させることになるでしょう。 この場合、(規約により) 各テストは test* という名前のメソッドにしておかなければなりません。

PHPUnit2_Framework_TestCasePHPUnit2_Framework_Test::countTestCases() を実装しており、 これは常に 1 を返します。このクラスで実装されている PHPUnit2_Framework_Test::run(PHPUnit2_Framework_TestResult $result) は、まず setUp() を実行し、テストメソッドを実行し、 それから tearDown() を実行し、その結果を PHPUnit2_Framework_TestResult に報告します。

PHPUnit2_Framework_TestCase によって実装されている外部プロトコルを 表 14.4 にまとめます。

表14.4 TestCase の外部プロトコル

メソッド意味
__construct()テストケースを作成します。
__construct(string $name)指定した名前のテストケースを作成します。この名前はテストケースを表示する際に使用されます。また、リフレクションで取得するテストメソッドの名前としても使用されます。
string getName()テストケースの名前を返します。
void setName($name)テストケースの名前を設定します。
PHPUnit2_Framework_TestResult run(PHPUnit2_Framework_TestResult $result)テストケースを実行し、結果を $result に格納するための便利なメソッドです。
void runTest()リフレクションによってテストメソッドを実行されたくない場合に、テストメソッドをオーバーライドします。

このクラスには 2 つのテンプレートメソッド setUp() および tearDown() が存在します。これをオーバーライドすると、 実行しようとしているテストに関する前処理や後始末を行うことができます。 表 14.5 にこれらのメソッドをまとめます。

表14.5 テンプレートメソッド

メソッド意味
void setUp()これをオーバーライドして、実行するテストに関連するオブジェクトの作成を行います。テストケース内で各テストが実行されるたびに、setUp() が毎回コールされます。
void tearDown()これをオーバーライドして、実行するテストに関連する、もう必要なくなったオブジェクトの後始末を行います。テストケース内で各テストが実行されるたびに、setUp() が毎回コールされます。一般に、tearDown() で明示的に後始末する必要があるのは外部リソース (例えばファイルやソケットなど) だけです。

PHPUnit2_Framework_TestSuite

PHPUnit2_Framework_TestSuite は複数の PHPUnit2_Framework_Test を組み合わせたものです。 簡単に言うと、このクラスには複数のテストケースが含まれており、 テストスイートを実行するとそれらの全てのテストが実行されます。 テストスイートは composite なので、テストスイートの中に別のテストスイートを含め、 さらにそのテストスイートの中には別のテストスイートが含まれており…… といったことも可能です。これにより、 いろいろなところから集めたテストをひとまとめにすることが簡単になります。

run(PHPUnit2_Framework_TestResult $result) および countTestCases() の 2 つに加え、 PHPUnit2_Framework_TestSuite は名前つきインスタンス、 名前なしインスタンスを作成するためのプロトコルも用意しています。 PHPUnit2_Framework_TestSuite のインスタンスを作成するための方法を 表 14.6 に示します。

表14.6 名前つき、あるいは名前なしインスタンスの作成

メソッド意味
__construct()空のテストスイートを返します。
__construct(string $theClass)test* という名前のメソッドを持つ、$theClass という名前のクラスのインスタンスを含むテストスイートを返します。$theClass という名前のクラスが存在しない場合は、$theClass という名前の空のテストスイートが返されます。
__construct(string $theClass, string $name)test* という名前のメソッドを持つ $theClass という名前のクラスのインスタンスを含む、$name という名前のテストスイートを返します。
__construct(ReflectionClass $theClass)test* という名前のメソッドを持つ、$theClass が指すクラスのインスタンスを含むテストスイートを返します。
__construct(ReflectionClass $theClass, $name)test* という名前のメソッドを持つ $theClass が指すクラスのインスタンスを含む、$name という名前のテストスイートを返します。
string getName()テストスイートの名前を返します。
void setName(string $name)テストスイートの名前を設定します。

PHPUnit2_Framework_TestSuite には、 PHPUnit2_Framework_Test を追加したり取得したりするためのプロトコルも用意されています。これを 表 14.7 にまとめます。

表14.7 テストの追加、取得のためのプロトコル

メソッド意味
void addTest(PHPUnit2_Framework_Test $test)テストスイートに $test を追加します。
void addTestFile(string $filename)指定したソースファイルで定義されているクラスをテストスイートに追加します。
void addTestFiles(array $filenames)指定したソースファイルで定義されているクラスをテストスイートに追加します。
int testCount()このテストスイートに直接登録されているテストの数を返します (再帰的には検索しません)。
PHPUnit2_Framework_Test[] tests()このテストスイートに直接登録されているテストを返します。
PHPUnit2_Framework_Test testAt(int $index)$index 番目のテストを返します。

例 14.3 に、テストスイートを作成して実行する方法を示します。

例 14.3: テストスイートの作成および実行

<?php
require_once 'PHPUnit2/Framework/TestSuite.php';
 
require_once 'ArrayTest.php';
 
// ArrayTest クラスのテストを含む
// テストスイートを作成します。
$suite = new PHPUnit2_Framework_TestSuite('ArrayTest');
 
// テストを実行します。
$suite->run();
?>

階層化されたテストケースを組み合わせた PHPUnit2_Framework_TestSuite の使用例として、PHPUnit 自身のテストスイートを見てみましょう。

例 14.4Tests/AllTests.php の一部を抜粋したもの、そして 例 14.5Tests/Framework/AllTests.php の一部を抜粋したものです。

例 14.4: AllTests クラス

<?php
if (!defined('PHPUnit2_MAIN_METHOD')) {
    define('PHPUnit2_MAIN_METHOD', 'AllTests::main');
}
 
require_once 'PHPUnit2/Framework/TestSuite.php';
require_once 'PHPUnit2/TextUI/TestRunner.php';
 
require_once 'Framework/AllTests.php';
// ...
 
class AllTests {
    public static function main() {
        PHPUnit2_TextUI_TestRunner::run(self::suite());
    }
 
    public static function suite() {
        $suite = new PHPUnit2_Framework_TestSuite('PHPUnit');
 
        $suite->addTest(Framework_AllTests::suite());
        // ...
 
        return $suite;
    }
}
 
if (PHPUnit2_MAIN_METHOD == 'AllTests::main') {
    AllTests::main();
}
?>

例 14.5: Framework_AllTests クラス

<?php
if (!defined('PHPUnit2_MAIN_METHOD')) {
    define('PHPUnit2_MAIN_METHOD', 'Framework_AllTests::main');
}
 
require_once 'PHPUnit2/Framework/TestSuite.php';
require_once 'PHPUnit2/TextUI/TestRunner.php';
 
require_once 'Framework/AssertTest.php';
// ...
 
class Framework_AllTests {
    public static function main() {
        PHPUnit2_TextUI_TestRunner::run(self::suite());
    }
 
    public static function suite() {
        $suite = new PHPUnit2_Framework_TestSuite('PHPUnit Framework');
 
        $suite->addTestSuite('Framework_AssertTest');
        // ...
 
        return $suite;
    }
}
 
if (PHPUnit2_MAIN_METHOD == 'Framework_AllTests::main') {
    Framework_AllTests::main();
}
?>

Framework_AssertTest クラスは、 PHPUnit2_Framework_TestCase を継承した一般的なテストケースです。

Tests/AllTests.php を実行すると、TextUI テストランナーを使用し、すべてのテストを実行します。一方 Tests/Framework/AllTests.php を実行すると、 PHPUnit2_Framework_* クラスのテストのみを実行します。

PHPUnit テストスイートの実行結果は、このようになります。

php AllTests.php
PHPUnit 2.3.0 by Sebastian Bergmann.

.........................................
.........................................
.......

Time: 4.642600

OK (89 tests)

PHPUnit2_Framework_TestResult

これらのテストを実行している間は、実行したテストの数・失敗したテスト・ テストの所要時間などをどこかに保存しておかなければなりません。 これらの結果を収集するのが PHPUnit2_Framework_TestResult です。ひとつの PHPUnit2_Framework_TestResult が、 テスト全体で使いまわされます。テストの実行結果や失敗の内容は PHPUnit2_Framework_TestResult に記録されていき、 実行が終了すると、PHPUnit2_Framework_TestResult には全てのテストの概要が含まれるようになります。

PHPUnit2_Framework_TestResult は、 テストの進行状況を知りたい他のオブジェクトから参照されることもあります。 例えば、グラフィカルなテストランナーは PHPUnit2_Framework_TestResult を監視し、各テストの開始時にプログレスバーを更新するでしょう。

表 14.8 は、 PHPUnit2_Framework_TestResult の外部プロトコルをまとめたものです。

表14.8 TestResult の外部プロトコル

メソッド意味
void addError(PHPUnit2_Framework_Test $test, Exception $e)実行中の $test から予期せぬ $e がスローされたことを記録します。
void addFailure(PHPUnit2_Framework_Test $test, PHPUnit2_Framework_AssertionFailedError $e)実行中の $test から予期せぬ $e がスローされたことを記録します。
PHPUnit2_Framework_TestFailure[] errors()記録されたエラーを返します。
PHPUnit2_Framework_TestFailure[] failures()記録された失敗を返します。
PHPUnit2_Framework_TestFailure[] notImplemented()記録された未完了テストを返します。
int errorCount()記録されたエラーの数を返します。
int failureCount()記録された失敗の数を返します。
int notImplementedCount()未完了のテストケースの数を返します。
int runCount()実行したテストケースの総数を返します。
boolean wasSuccessful()すべてのテストの実行に成功したかどうかを返します。
boolean allCompletlyImplemented()すべてのテストが完全に実装されているかどうかを返します。
void collectCodeCoverageInformation(bool $flag)コードカバレッジ情報の収集を有効あるいは無効にします。
array getCodeCoverageInformation()収集したコードカバレッジ情報を返します。

PHPUnit2_Framework_TestResult のオブザーバを登録したい場合は、PHPUnit2_Framework_TestListener を実装する必要があります。これを登録するには、 表 14.9 に示した addListener() を使用します。

表14.9 TestResult および TestListener

メソッド意味
void addListener(PHPUnit2_Framework_TestListener $listener)$listener を登録し、テスト結果の内容が更新された場合にその内容を受け取るようにします。
void removeListener(PHPUnit2_Framework_TestListener $listener)更新を受け取る $listener の登録を解除します。

表 14.10 に、テストリスナーが実装するメソッドを示します。 例 15.3 も参照ください。

表14.10 TestListener のコールバック

メソッド意味
void addError(PHPUnit2_Framework_Test $test, Exception $e)$test$e をスローしました。
void addFailure(PHPUnit2_Framework_Test $test, PHPUnit2_Framework_AssertionFailedError $e)$test がアサーションに失敗し、PHPUnit2_Framework_AssertionFailedError 系がスローされました。
void addIncompleteTest(PHPUnit2_Framework_Test $test, Exception $e)$test は完了しませんでした。
void startTestSuite(PHPUnit2_Framework_TestSuite $suite)$suite の実行が始まります。
void endTestSuite(PHPUnit2_Framework_TestSuite $suite)$suite の実行が終了しました。
void startTest(PHPUnit2_Framework_Test $test)$test の実行が始まります。
void endTest(PHPUnit2_Framework_Test $test)$test の実行が終了しました。

パッケージの構成

この本で取り上げたクラスの多くは PHPUnit2/Framework にあるものです。ここには PHPUnit のすべてのパッケージが含まれています。

  • PHPUnit2/Framework

    PHPUnit の基本クラス。

  • PHPUnit2/Extensions

    PHPUnit フレームワークの拡張。

  • PHPUnit2/Runner

    テストの実行を抽象化したクラス。

  • PHPUnit2/TextUI

    テキストベースのテストランナー。

  • PHPUnit2/Util

    他のパッケージから使用するユーティリティクラス群。

第15章 PHPUnit の拡張

テストを書きやすくする、あるいはテストの実行結果の表示方法を変更するなど、 PHPUnit はさまざまな方法で拡張することができます。 PHPUnit を拡張するための第一歩をここで説明します。

PHPUnit2_Framework_TestCase のサブクラスの作成

PHPUnit2_Framework_TestCase を継承した抽象サブクラスにユーティリティメソッドを書き、 そのクラスをさらに継承してテストクラスを作成します。 これが、PHPUnit を拡張するための一番簡単な方法です。

アサートクラスの作成

あなたの目的に合った独自のアサーションを実装したクラスを作成します。

PHPUnit2_Extensions_TestDecorator のサブクラスの作成

PHPUnit2_Extensions_TestDecorator のサブクラスでテストケースあるいはテストスイートをラッピングし、 デコレータパターンを使用することで 各テストの実行前後に何らかの処理をさせることができます。

PHPUnit には、PHPUnit2_Extensions_RepeatedTest および PHPUnit2_Extensions_TestSetup という 2 つの具象テストデコレータが付属しています。 前者はテストを繰り返し実行し、それらが全て成功した場合にのみ成功とみなします。 後者については 第 5 章 で説明しました。

例 15.1 は、テストデコレータ PHPUnit2_Extensions_RepeatedTest の一部を抜粋したものです。独自のデコレータを作成するための参考にしてください。

例 15.1: RepeatedTest デコレータ

<?php
require_once 'PHPUnit2/Extensions/TestDecorator.php';
 
class PHPUnit2_Extensions_RepeatedTest extends PHPUnit2_Extensions_TestDecorator {
    private $timesRepeat = 1;
 
    public function __construct(PHPUnit2_Framework_Test $test, $timesRepeat = 1) {
        parent::__construct($test);
 
        if (is_integer($timesRepeat) &&
            $timesRepeat >= 0) {
            $this->timesRepeat = $timesRepeat;
        }
    }
 
    public function countTestCases() {
        return $this->timesRepeat * $this->test->countTestCases();
    }
 
    public function run($result = NULL) {
        if ($result === NULL) {
            $result = $this->createResult();
        }
 
        for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
            $this->test->run($result);
        }
 
        return $result;
    }
}
?>

PHPUnit2_Framework_Test を実装する

PHPUnit2_Framework_Test インターフェイスの機能は限られており、 実装するのは簡単です。PHPUnit2_Framework_Test を実装するのは PHPUnit2_Framework_TestCase の実装より単純で、 これを用いて例えば データ駆動のテスト (data-driven tests) などを実行します。

カンマ区切り (CSV) ファイルの値と比較する、データ駆動のテストを 例 15.2 に示します。このファイルの各行は foo;bar のような形式になっており (訳注: CSV じゃない……)、 最初の値が期待値で 2 番目の値が実際の値です。

例 15.2: データ駆動のテスト

<?php
require_once 'PHPUnit2/Framework/Assert.php';
require_once 'PHPUnit2/Framework/Test.php';
require_once 'PHPUnit2/Framework/TestResult.php';
 
class DataDrivenTest implements PHPUnit2_Framework_Test {
    private $lines;
 
    public function __construct($dataFile) {
        $this->lines = file($dataFile);
    }
 
    public function countTestCases() {
        return sizeof($this->lines);
    }
 
    public function run($result = NULL) {
        if ($result === NULL) {
            $result = new PHPUnit2_Framework_TestResult;
        }
 
        $result->startTest($this);
 
        foreach ($this->lines as $line) {
            list($expected, $actual) = explode(';', $line);
 
            try {
                PHPUnit2_Framework_Assert::assertEquals(trim($expected), trim($actual));
            }
 
            catch (PHPUnit2_Framework_ComparisonFailure $e) {
                $result->addFailure($this, $e);
            }
 
            catch (Exception $e) {
                $result->addError($this, $e);
            }
        }
 
        $result->endTest($this);
 
        return $result;
    }
}
 
$test   = new DataDrivenTest('data_file.csv');
$result = $test->run();
 
$failures = $result->failures();
print $failures[0]->thrownException()->toString();
?>
expected: <foo> but was: <bar>

PHPUnit2_Framework_TestResult のサブクラスの作成

独自の PHPUnit2_Framework_TestResult オブジェクトを run() メソッドに渡すと、 テストの実行方法や収集されるテスト結果を変更することができます。

PHPUnit2_Framework_TestListener の実装

テスト結果をカスタマイズするために、必ず PHPUnit2_Framework_TestResult のサブクラスを書かなければならないというわけではありません。たいていは、 新しい PHPUnit2_Framework_TestListener を実装して (表 14.10 を参照ください)、 テストの前にそれを PHPUnit2_Framework_TestResult オブジェクトにアタッチするだけで十分です。

例 15.3 は、PHPUnit2_Framework_TestListener インターフェイスを実装する単純な例です。

例 15.3: シンプルなテストリスナー

<?php
require_once 'PHPUnit2/Framework/TestListener.php';
 
class SimpleTestListener
implements PHPUnit2_Framework_TestListener {
  public function
  addError(PHPUnit2_Framework_Test $test, Exception $e) {
    printf(
      "テスト '%s' の実行中にエラーが発生しました。\n",
      $test->getName()
    );
  }
 
  public function
  addFailure(PHPUnit2_Framework_Test $test,
             PHPUnit2_Framework_AssertionFailedError $e) {
    printf(
      "テスト '%s' に失敗しました。\n",
      $test->getName()
    );
  }
 
  public function
  addIncompleteTest(PHPUnit2_Framework_Test $test,
                    Exception $e) {
    printf(
      "テスト '%s' は未完了です。\n",
      $test->getName()
    );
  }
 
  public function startTest(PHPUnit2_Framework_Test $test) {
    printf(
      "テスト '%s' が開始されました。\n",
      $test->getName()
    );
  }
 
  public function endTest(PHPUnit2_Framework_Test $test) {
    printf(
      "テスト '%s' が終了しました。\n",
      $test->getName()
    );
  }
 
  public function
  startTestSuite(PHPUnit2_Framework_TestSuite $suite) {
    printf(
      "テストスイート '%s' が開始されました。\n",
      $suite->getName()
    );
  }
 
  public function
  endTestSuite(PHPUnit2_Framework_TestSuite $suite) {
    printf(
      "テストスイート '%s' が終了しました。\n",
      $suite->getName()
    );
  }
}
?>

例 15.4 は、テストスイートを実行して監視する方法を示したものです。

例 15.4: テストスイートの実行と監視

<?php
require_once 'PHPUnit2/Framework/TestResult.php';
require_once 'PHPUnit2/Framework/TestSuite.php';
 
require_once 'ArrayTest.php';
require_once 'SimpleTestListener.php';
 
// ArrayTest クラスのテストを含む
// テストスイートを作成します。
$suite = new PHPUnit2_Framework_TestSuite('ArrayTest');
 
// テスト結果オブジェクトを作成し、そこにオブザーバとして
// SimpleTestListener をアタッチします。
$result = new PHPUnit2_Framework_TestResult;
$result->addListener(new SimpleTestListener);
 
// テストを実行します。
$suite->run($result);
?>
テストスイート 'ArrayTest' が開始されました。
テスト 'testNewArrayIsEmpty' が開始されました。
テスト 'testNewArrayIsEmpty' が終了しました。
テスト 'testArrayContainsAnElement' が開始されました。
テスト 'testArrayContainsAnElement' が終了しました。
テストスイート 'ArrayTest' が終了しました。

新しいテストランナーの作成

テストの実行結果を異なる方法で受け取りたい場合には、 独自のテストランナーを作成します。その際の初めの一歩となるのが、 PHPUnit2_TextUI_TestRunner クラス (PHPUnit のコマンドライン版テストランナー) の親クラスである抽象クラス PHPUnit2_Runner_BaseTestRunner です。

付録A PHP 4 用の PHPUnit

PHP 5 でなくても動作する、PHP 4 用の PHPUnit もあります。 PHP 4 のオブジェクトモデルには制限があるので、PHP 4 用の PHPUnit は (PHP 5 版とは異なり) JUnit の完全な移植ではありません。また、PHP 5 版の機能のうち、コードカバレッジ機能などはありません。

PEAR インストーラで PHP 4 用の PHPUnit をインストールするには、 次のコマンドを実行します。

pear install -f http://pear.phpunit.de/get/PHPUnit-1.3.3.tgz

PHP 4 用の PHPUnit で使われるテストケースクラスは、PHP 5 用の PHPUnit で使われるものと似ています。本質的な違いは、こちらのクラスは PHPUnit_TestCase (このクラス自身は PHPUnit_Assert を継承しており、 ここでアサーションメソッドが定義されています) を継承しているということです。

ArrayTest テストケースを、PHP 4 用の PHPUnit で使用できるように書き直したものが 例 A.1 です。

例 A.1: PHPUnit 1.x 用のテストを書く

<?php
require_once 'PHPUnit/TestCase.php';
 
class ArrayTest extends PHPUnit_TestCase
{
    var $_fixture;
 
    function setUp()
    {
        $this->_fixture = array();
    }
 
    function testNewArrayIsEmpty()
    {
        $this->assertEquals(0, sizeof($this->_fixture));
    }
 
    function testArrayContainsAnElement()
    {
        $this->_fixture[] = 'Element';
        $this->assertEquals(1, sizeof($this->_fixture));
    }
}
?>

PHP 4 用の PHPUnit には、TextUI テストランナーがありません。 PHP 4 用の PHPUnit でテストを実行する際の一般的な方法は、 テストスイートを書いた後で、それを 例 A.2 のように手動で実行することです。

例 A.2: PHPUnit 1.x のテストケースを実行する

<?php
require_once 'ArrayTest.php';
require_once 'PHPUnit.php';
 
$suite  = new PHPUnit_TestSuite('ArrayTest');
$result = PHPUnit::run($suite);
 
print $result->toString();
?>
TestCase arraytest->testnewarrayisempty() passed
TestCase arraytest->testarraycontainsanelement() passed

PHP 4 用の PHPUnit にはあって PHP 5 用の PHPUnit にはまだ存在しない機能のひとつに、 図 A.1 のようなグラフィカルなテストランナーがあります。 これは PHP-GTK で作成されています。

図A.1 PHP-GTK のテストランナー

PHP-GTK のテストランナー

付録B 目次

A

Agile Documentation (アジャイルな文書作成), コマンドラインのテストランナー, アジャイルな文書作成
Assertions, PHPUnit2_Framework_Assert
Assertions (アサーション), 自動テスト, アサートクラスの作成
Automated Documentation, アジャイルな文書作成
Automated Test (自動テスト), 自動テスト

D

Data-Driven Tests (データ駆動のテスト), PHPUnit2_Framework_Test を実装する
Design-by-Contract (規約による設計), テストファーストプログラミング, PHPUnit2_Framework_Assert
Documenting Assumptions, アジャイルな文書作成

E

Error (エラー), コマンドラインのテストランナー
Extreme Programming (エクストリームプログラミング), テストファーストプログラミング, アジャイルな文書作成

I

Incomplete Test (不完全なテスト), コマンドラインのテストランナー, 不完全なテスト

O

Observer Pattern (オブザーバパターン), 自己シャント, PHPUnit2_Framework_TestResult

R

Refactoring (リファクタリング), リファクタリング

S

Self Shunt Pattern (自己シャントパターン), 自己シャント
setUp(), Fixtures, PHPUnit の実装
Skeleton Generator (雛形ジェネレータ), コマンドラインのテストランナー
Stubs (スタブ), スタブ, 複数チームでのテスト

T

tearDown(), Fixtures
Template Method (テンプレートメソッド), Fixtures
Test Report (テストレポート), 結果を整形する
Test-Driven Development (テスト駆動開発), テストファーストプログラミング
Test-First Programming (テストファーストプログラミング), テストファーストプログラミング
TestDox, アジャイルな文書作成

付録C 参考文献

[Astels2003] Test Driven Development. Astels David [FAMILY Given]. 製作著作 © 2003. Prentice Hall. ISBN 0131016490.

[Beck1997] Smalltalk Best Practice Patterns. Beck Kent [FAMILY Given]. 製作著作 © 1997. Prentice Hall. ISBN 0-13-476904-X.

[Beck1997-ja] ケント・ベックのSmalltalkベストプラクティス・パターン―シンプル・デザインへの宝石集. Beck Kent [FAMILY Given]. 製作著作 © 2003. ピアソンエデュケーション. ISBN 4894717549.

[Beck2002] Test Driven Development by Example. Beck Kent [FAMILY Given]. 製作著作 © 2002. Addison-Wesley. ISBN 0-321-14653-0.

[Beck2002-ja] テスト駆動開発入門. Beck Kent [FAMILY Given]. 製作著作 © 2003. ピアソンエデュケーション. ISBN 4894717115.

[Beck2004] JUnit Pocket Guide. Quick Lookup and Advice. Beck Kent [FAMILY Given]. 製作著作 © 2004. O'Reilly Media. ISBN 0-596-00743-4.

[Bergmann2005] Professionelle Softwareentwicklung mit PHP 5. Objektorientierung, Entwurfsmuster, Modellierung, Fortgeschrittene Datenbankprogrammierung. Bergmann Sebastian [FAMILY Given]. 製作著作 © 2005. dpunkt.verlag. ISBN 3-89864-229-1.

[GammaBeck1999] JUnit: A Cook's Tour. Gamma Erich [FAMILY Given] 、 Beck Kent [FAMILY Given]. 製作著作 © 1999. http://junit.sourceforge.net/doc/cookstour/cookstour.htm.

[GuBaRe2005] PHP 5 Power Programming. Gutmans Andi [FAMILY Given], Bakken Stig [FAMILY Given], 、 Rethans Derick [FAMILY Given]. 製作著作 © 2005. Prentice Hall. ISBN 0-131-47149-X.

付録D 著作権

Copyright (c) 2005-2009 Sebastian Bergmann.

この作品は、Creative Commons Attribution License の下で
ライセンスされています。このライセンスの内容を確認するには、
http://creativecommons.org/licenses/by/2.0/ を訪問するか、あるいは
Creative Commons, 559 Nathan Abbott Way, Stanford, California 94305,
USA.
に手紙を送ってください。

このライセンスの概要を以下に示します。その後に、完全な文書を示します。

--------------------------------------------------------------------

あなたは以下の条件に従う場合に限り、自由に

    * 本作品を複製、頒布、展示、実演することができます。
    * 二次的著作物を作成することができます。
    * 本作品を営利目的で利用することができます。

あなたの従うべき条件は以下の通りです。

帰属. あなたは原著作者のクレジットを表示しなければなりません。

    * 再利用や頒布にあたっては、この作品の使用許諾条件を他の人々に
      明らかにしなければなりません。

    * 著作[権]者から許可を得ると、これらの条件は適用されません。

上記によってあなたのフェアユースその他の権利が影響を受けることは
まったくありません。

これは、以下に示す完全なライセンスの要約です。

====================================================================

Creative Commons Legal Code
Attribution 2.0

CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
DAMAGES RESULTING FROM ITS USE.

License

THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
PROHIBITED.

BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS
YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
SUCH TERMS AND CONDITIONS.

1. Definitions

   a. "Collective Work" means a work, such as a periodical issue,
      anthology or encyclopedia, in which the Work in its entirety in
      unmodified form, along with a number of other contributions,
      constituting separate and independent works in themselves, are
      assembled into a collective whole. A work that constitutes a
      Collective Work will not be considered a Derivative Work (as
      defined below) for the purposes of this License.

   b. "Derivative Work" means a work based upon the Work or upon the
      Work and other pre-existing works, such as a translation,
      musical arrangement, dramatization, fictionalization, motion
      picture version, sound recording, art reproduction, abridgment,
      condensation, or any other form in which the Work may be recast,
      transformed, or adapted, except that a work that constitutes a
      Collective Work will not be considered a Derivative Work for the
      purpose of this License. For the avoidance of doubt, where the
      Work is a musical composition or sound recording, the
      synchronization of the Work in timed-relation with a moving
      image ("synching") will be considered a Derivative Work for the
      purpose of this License.

   c. "Licensor" means the individual or entity that offers the Work
      under the terms of this License.

   d. "Original Author" means the individual or entity who created the Work.

   e. "Work" means the copyrightable work of authorship offered under
      the terms of this License.

   f. "You" means an individual or entity exercising rights under this
      License who has not previously violated the terms of this
      License with respect to the Work, or who has received express
      permission from the Licensor to exercise rights under this
      License despite a previous violation.

2. Fair Use Rights. Nothing in this license is intended to reduce,
   limit, or restrict any rights arising from fair use, first sale or
   other limitations on the exclusive rights of the copyright owner
   under copyright law or other applicable laws.

3. License Grant. Subject to the terms and conditions of this License,
   Licensor hereby grants You a worldwide, royalty-free,
   non-exclusive, perpetual (for the duration of the applicable
   copyright) license to exercise the rights in the Work as stated
   below:

   a. to reproduce the Work, to incorporate the Work into one or more
      Collective Works, and to reproduce the Work as incorporated in
      the Collective Works;

   b. to create and reproduce Derivative Works;

   c. to distribute copies or phonorecords of, display publicly,
      perform publicly, and perform publicly by means of a digital
      audio transmission the Work including as incorporated in
      Collective Works;

   d. to distribute copies or phonorecords of, display publicly,
      perform publicly, and perform publicly by means of a digital
      audio transmission Derivative Works.

   e.

      For the avoidance of doubt, where the work is a musical composition:

         i. Performance Royalties Under Blanket Licenses. Licensor
            waives the exclusive right to collect, whether
            individually or via a performance rights society
            (e.g. ASCAP, BMI, SESAC), royalties for the public
            performance or public digital performance (e.g. webcast)
            of the Work.

        ii. Mechanical Rights and Statutory Royalties. Licensor waives
            the exclusive right to collect, whether individually or
            via a music rights agency or designated agent (e.g. Harry
            Fox Agency), royalties for any phonorecord You create from
            the Work ("cover version") and distribute, subject to the
            compulsory license created by 17 USC Section 115 of the US
            Copyright Act (or the equivalent in other jurisdictions).

   f. Webcasting Rights and Statutory Royalties. For the avoidance of
      doubt, where the Work is a sound recording, Licensor waives the
      exclusive right to collect, whether individually or via a
      performance-rights society (e.g. SoundExchange), royalties for
      the public digital performance (e.g. webcast) of the Work,
      subject to the compulsory license created by 17 USC Section 114
      of the US Copyright Act (or the equivalent in other
      jurisdictions).

The above rights may be exercised in all media and formats whether now
known or hereafter devised. The above rights include the right to make
such modifications as are technically necessary to exercise the rights
in other media and formats. All rights not expressly granted by
Licensor are hereby reserved.

4. Restrictions.The license granted in Section 3 above is expressly
   made subject to and limited by the following restrictions:

   a. You may distribute, publicly display, publicly perform, or
      publicly digitally perform the Work only under the terms of this
      License, and You must include a copy of, or the Uniform Resource
      Identifier for, this License with every copy or phonorecord of
      the Work You distribute, publicly display, publicly perform, or
      publicly digitally perform. You may not offer or impose any
      terms on the Work that alter or restrict the terms of this
      License or the recipients' exercise of the rights granted
      hereunder. You may not sublicense the Work. You must keep intact
      all notices that refer to this License and to the disclaimer of
      warranties. You may not distribute, publicly display, publicly
      perform, or publicly digitally perform the Work with any
      technological measures that control access or use of the Work in
      a manner inconsistent with the terms of this License
      Agreement. The above applies to the Work as incorporated in a
      Collective Work, but this does not require the Collective Work
      apart from the Work itself to be made subject to the terms of
      this License. If You create a Collective Work, upon notice from
      any Licensor You must, to the extent practicable, remove from
      the Collective Work any reference to such Licensor or the
      Original Author, as requested. If You create a Derivative Work,
      upon notice from any Licensor You must, to the extent
      practicable, remove from the Derivative Work any reference to
      such Licensor or the Original Author, as requested.

   b. If you distribute, publicly display, publicly perform, or
      publicly digitally perform the Work or any Derivative Works or
      Collective Works, You must keep intact all copyright notices for
      the Work and give the Original Author credit reasonable to the
      medium or means You are utilizing by conveying the name (or
      pseudonym if applicable) of the Original Author if supplied; the
      title of the Work if supplied; to the extent reasonably
      practicable, the Uniform Resource Identifier, if any, that
      Licensor specifies to be associated with the Work, unless such
      URI does not refer to the copyright notice or licensing
      information for the Work; and in the case of a Derivative Work,
      a credit identifying the use of the Work in the Derivative Work
      (e.g., "French translation of the Work by Original Author," or
      "Screenplay based on original Work by Original Author"). Such
      credit may be implemented in any reasonable manner; provided,
      however, that in the case of a Derivative Work or Collective
      Work, at a minimum such credit will appear where any other
      comparable authorship credit appears and in a manner at least as
      prominent as such other comparable authorship credit.

5. Representations, Warranties and Disclaimer

UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY,
OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED
WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.

6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
   APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
   LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
   OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE
   WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
   DAMAGES.

7. Termination

   a. This License and the rights granted hereunder will terminate
      automatically upon any breach by You of the terms of this
      License. Individuals or entities who have received Derivative
      Works or Collective Works from You under this License, however,
      will not have their licenses terminated provided such
      individuals or entities remain in full compliance with those
      licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any
      termination of this License.

   b. Subject to the above terms and conditions, the license granted
      here is perpetual (for the duration of the applicable copyright
      in the Work). Notwithstanding the above, Licensor reserves the
      right to release the Work under different license terms or to
      stop distributing the Work at any time; provided, however that
      any such election will not serve to withdraw this License (or
      any other license that has been, or is required to be, granted
      under the terms of this License), and this License will continue
      in full force and effect unless terminated as stated above.

8. Miscellaneous

   a. Each time You distribute or publicly digitally perform the Work
      or a Collective Work, the Licensor offers to the recipient a
      license to the Work on the same terms and conditions as the
      license granted to You under this License.

   b. Each time You distribute or publicly digitally perform a
      Derivative Work, Licensor offers to the recipient a license to
      the original Work on the same terms and conditions as the
      license granted to You under this License.

   c. If any provision of this License is invalid or unenforceable
      under applicable law, it shall not affect the validity or
      enforceability of the remainder of the terms of this License,
      and without further action by the parties to this agreement,
      such provision shall be reformed to the minimum extent necessary
      to make such provision valid and enforceable.

   d. No term or provision of this License shall be deemed waived and
      no breach consented to unless such waiver or consent shall be in
      writing and signed by the party to be charged with such waiver
      or consent.

   e. This License constitutes the entire agreement between the
      parties with respect to the Work licensed here. There are no
      understandings, agreements or representations with respect to
      the Work not specified here. Licensor shall not be bound by any
      additional provisions that may appear in any communication from
      You. This License may not be modified without the mutual written
      agreement of the Licensor and You.

Creative Commons is not a party to this License, and makes no warranty
whatsoever in connection with the Work. Creative Commons will not be
liable to You or any party on any legal theory for any damages
whatsoever, including without limitation any general, special,
incidental or consequential damages arising in connection to this
license. Notwithstanding the foregoing two (2) sentences, if Creative
Commons has expressly identified itself as the Licensor hereunder, it
shall have all rights and obligations of Licensor.

Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, neither party will use the trademark
"Creative Commons" or any related trademark or logo of Creative
Commons without the prior written consent of Creative Commons. Any
permitted use will be in compliance with Creative Commons'
then-current trademark usage guidelines, as may be published on its
website or otherwise made available upon request from time to time.

Creative Commons may be contacted at http://creativecommons.org/.

====================================================================