製作著作 © 2005, 2006, 2007, 2008, 2009 Sebastian Bergmann
PHPUnit 3.3 対応版 Updated on 2009-12-29.
どんなにすぐれたプログラマも、間違いを犯します。 よいプログラマとそうでないプログラマの違いは、 よいプログラマはテストを行って間違いをできるだけ早く発見してしまうことです。 テストをするのが早ければ早いほど間違いを発見しやすくなり、 またそれを修正しやすくなります。 リリース直前までテストを先延ばしにしておくことが非常に問題であるのはこのためです。 そんなことをすると、すべてのエラーを発見しきることができず、 発見したエラーを修正することも非常に難しくなります。結局は、 トリアージを行ってどのエラーに対応するかを判断しなければならなくなります。 なぜならすべてのエラーを完全に修正することは不可能だからです。
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.');
}
}
?>
これで、テストは完全に自動化されました。最初のバージョンでは単に テストする だけでしたが、このバージョンでは 自動テスト になっています。
自動テストを行う目的は、間違いを少なくすることです。 いくらすばらしいテストを行ったところで あなたのコードが完璧なものになるわけではありませんが、 自動テストを始めることで不具合の量を劇的に減らすことになるでしょう。 自動テストによってあなたのコードは信頼性の高いものとなり、大胆な設計変更 (リファクタリング) を行ったりチームメイトとの関係をよりよくしたり (複数チームでのテスト)、 その日の朝に比べて帰宅前のコードがよりよくなっていることを確信できたりといった効果があります。
いまのところ、組み込みの array および
sizeof() 関数のテストしかありません。
PHP が提供する数多くの array_*()
関数をテストしようとすると、それらそれぞれについてテストを記述する必要があります。
それらのすべてのテストについて基盤部分を最初から書いていくこともできますが、
共通部分は一度だけ記述するようにし、
個々のテストではテスト固有の部分のみを記述していくほうがずっとよい方法です。
PHPUnit は、そのような基盤部分を提供します。
PHPUnit のようなフレームワークには、解決しなければならない制約があります。 その中のいくつかはお互いに相反するものです。 テストは、以下の条件を同時に満たす必要があります。
テストの書き方を身に着けるのが難しければ、 開発者はそんなものを覚えようとしないでしょう。
テストが書きにくければ、開発者はそんなものを書こうとしないでしょう。
テストコードには、外部からの要素を含めるべきではありません。 そうするとテストコードが周りのノイズに埋もれてしまいます。
ボタン一発でテストが実行でき、 その結果は明白な形式で表示されなければなりません。
一日に何百何千というテストを実行できるよう、 テストはすばやく実行できなければなりません。
テストは、お互い他のテストに影響を及ぼしてはいけません。 テストの実行順序を変えることでテストの結果が変わってしまってはいけません。
テストの数や組み合わせを自由に選択できなければなりません。 テストが独立している以上、これは当然のことです。
これらの制約の中には、相反する項目が 2 点あります。
一般に、テストにはプログラミング言語の全機能を必要としません。 多くのテストツールは、テストを記述するため必要最小限の機能のみを組み込んだ 独自のスクリプト言語を提供しています。その結果、テストは読みやすく、 また書きやすいものになります。 なぜならテストの内容から気をそらせるノイズがないためです。しかし、 またひとつ新たなプログラミング言語やプログラミングツールの使い方を覚える必要があり、 不便です。
ひとつのテストが他のテストの結果に何の影響もおよぼさないようにするには、 各テストの実行前に毎回テスト用の環境を構築し、 終了後には毎回それを元の状態に戻す必要があります。しかし、環境を構築する (例: データベースに接続し、特定の状態を表すデータを投入する) には長い時間がかかります。
PHPUnit は、テスト言語として PHP を使用することで これらの衝突を回避しようとしています。小規模で単純なテストを行う際には、 PHP の機能は行き過ぎた面もあるかもしれません。しかし、PHP を使用することで、 これまでの開発経験や開発ツールを武器として利用できます。 あまり気乗りしないテスターを納得させるため、 テストを最初に書き始める際の負担をできるだけ下げることが重要だと考えています。
PHPUnit では、実行速度よりもテストの独立性を重視しています。 独立したテストに価値があるのは、そのほうがより高品質のフィードバックが得られるからです。 一連のテストの最初のほうで失敗したことでその後のすべてのテストが失敗してしまい、 大量の失敗報告を受け取るようなことがなくなります。 このオリエンテーションでは独立したテストを目指し、 シンプルなオブジェクトを数多く作るという設計を心がけます。 各オブジェクトは独立してすばやくテストできます。結果としてよりよい設計 に加えてより高速なテストが可能となります。
PHPUnit ではほとんどのテストが成功することを想定しており、 成功したテストの詳細について報告することはあまり価値がないと考えています。 テストが失敗した場合には、そのことをしっかり報告しなければなりません。 大半のテストは成功すべきであり、 それらについては実行したテストの数以外に特にコメントすべき情報はありません。 これは、PHPUnit のコアではなく報告用のクラス群に組み込まれている機能です。 テストの結果が表示される際には実行したテストの数が表示されますが、 詳細が表示されるのは失敗したテストについてだけです。
テストは決め細やかに行うこと、つまりひとつのテストではひとつのオブジェクトの ひとつの機能についてをテストするようにすることが期待されています。 そのため、最初のテストは失敗し、テストの実行は終了し、 PHPUnit は失敗を報告します。多くの小さなテストを実行させることは一種の芸術です。 決め細やかなテストにより、システム全体の設計がよりよいものとなります。
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 から
PHPUnit や PHPUnit2
をインストールしている場合、まずそれをアンインストールしなければなりません。
インストールすると、PHPUnit のソースファイルがローカルの PEAR
ディレクトリに格納されます。場所は、通常は
/usr/lib/php/PHPUnit です。
PHPUnit がサポートしているのは PEAR インストーラを使用する方法のみですが、 PHPUnit を手動でインストールすることも可能です。そのためには、 以下の手順に従ってください。
http://pear.phpunit.de/get/
からアーカイブをダウンロードし、それを php.ini
設定ファイルの include_path
で指定したディレクトリに展開します。
phpunit スクリプトを準備します。
pear-phpunit スクリプトの名前を
phpunit に変更します。
その中の @php_bin@ という文字列を、
PHP コマンドラインインタプリタへのパス (通常は
/usr/bin/php) に変更します。
それを PATH の通ったディレクトリにコピーし、
実行可能属性を付与します (chmod +x phpunit)。
例 4.1 は、PHPUnit を使用する形式で 例 1.4 の 2 つのテストを書き直したものです。
例 4.1: PHPUnit を使用した Array および sizeof() のテスト
<?php
require_once 'PHPUnit/Framework.php';
class ArrayTest extends PHPUnit_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));
}
}
?>
Whenever you are tempted to type something into a
何かを | ||
| --Martin Fowler | ||
例 4.1 では、 PHPUnit を使用してテストを記述する基本手順を説明しています。
Class という名前のクラスのテストは、ClassTest という名前のクラスに記述します。
ClassTest は、(ほとんどの場合) PHPUnit_Framework_TestCase を継承します。
テストは、引数をとらないパブリックメソッドとして test* という名前で記述します。
また、@test アノテーションをメソッドのコメント部で使用することで、それがテストメソッドであることを示すことができます。
テストメソッドの中で assertEquals() のようなアサーションメソッド (表 20.1 を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。
PHPUnit バージョン 2.0-2.3 では、クラス名が
PHPUnit_ ではなく PHPUnit2_
で始まっていたことに注意しましょう。既存のテストケースは
そのままでも PHPUnit バージョン 3.0 で動作しますが、
新しいクラス名に変更することを強く推奨します。
phpunit コマンドを実行すると、PHPUnit
のコマンドライン版テストランナーが起動します。
コマンドラインのテストランナーを使用したテストの様子を以下に示します。
phpunit ArrayTest
PHPUnit 3.0.0 by Sebastian Bergmann.
..
Time: 00:00
OK (2 tests)テストがひとつ実行されるたびに、PHPUnit コマンドラインツールはその経過を示す文字を出力します。
PHPUnit は、失敗 (failures) と
エラー (errors) を区別します。
「失敗」は PHPUnit のアサーションに違反した場合、つまり例えば
assertEquals() のコールに失敗した場合などで、
「エラー」は予期せぬ例外や PHP のエラーが発生した場合となります。
この区別は、時に有用です。というのは「エラー」は一般的に「失敗」
より修正しやすい傾向があるからです。
もし大量の問題が発生した場合は、まず「エラー」を最初に片付け、
その後で「失敗」を修正していくのが最良の方法です。
以下のコードで、コマンドライン版テストランナーのスイッチの一覧を見てみましょう。
phpunit --help
PHPUnit 3.0.0 by Sebastian Bergmann.
Usage: phpunit [switches] UnitTest [UnitTest.php]
--log-graphviz <file> Log test execution in GraphViz markup.
--log-json <file> Log test execution in JSON format.
--log-tap <file> Log test execution in TAP format to file.
--log-xml <file> Log test execution in XML format to file.
--report <dir> Generate combined test/coverage report in HTML format.
--testdox-html <file> Write agile documentation in HTML format to file.
--testdox-text <file> Write agile documentation in Text format to file.
--filter <pattern> Filter which tests to run.
--loader <loader> TestSuiteLoader implementation to use.
--repeat <times> Runs the test(s) repeatedly.
--tap Report test execution progress in TAP format.
--verbose Output more verbose information.
--wait Waits for a keystroke after each test.
--skeleton Generate skeleton UnitTest class for Unit in Unit.php.
--help Prints this usage information.
--version Prints the version and exits.
-d key[=value] Sets a php.ini value.phpunit UnitTest
UnitTest という名前のクラスで定義されている
テストを実行します。このクラスは、UnitTest.php
という名前のファイルの中に定義されているものとします。
UnitTest は、PHPUnit_Framework_TestCase
を継承したクラスであるか、あるいは PHPUnit_Framework_Test
オブジェクト、例えば PHPUnit_Framework_TestSuite
のインスタンスを返す public static suite()
というメソッドを保持するクラスでなければなりません。
phpunit UnitTest UnitTest.php
UnitTest という名前のクラスで定義されているテストを実行します。
このクラスは、指定したファイルの中で定義されているものとします。
--log-graphviz
GraphViz
のマークアップを使用して、テストの実行結果のログを作成します。
生成されたログファイルは、例えば
dot などを使用して表示することが可能です。
詳細は 第 15 章 を参照ください。
このパラメータは、PEAR パッケージ Image_GraphViz
がインストールされている場合にのみ使用可能となることに注意しましょう。
--log-json--log-tapTest Anything Protocol (TAP) フォーマットを使用して、テストの実行結果のログを作成します。 詳細は 第 15 章 を参照ください。
--log-xmlテストの実行結果を XML 形式のログファイルに出力します。 詳細は 第 15 章 を参照ください。
--reportレポートを作成します。 PHP の Xdebug 拡張モジュールが使用可能な場合は、 テスト結果とコードカバレッジを組み合わせたレポートとなります。 詳細は 第 13 章 を参照ください。
このパラメータは、Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--testdox-html および --testdox-text実行したテストについて、HTML あるいはプレーンテキスト形式のドキュメントを生成します 詳細は 第 14 章 を参照ください。
--filter指定したパターンにマッチする名前のテストのみを実行します。 パターンとして指定できるのは、単一のテスト名か、 あるいは複数のテスト名にマッチする 正規表現 です。
--loader
PHPUnit_Runner_TestSuiteLoader を実装したクラスのうち、
実際に使用するものを指定します。
標準のテストスイートローダーは、現在の作業ディレクトリおよび PHP
の設定項目 include_path
で指定されているディレクトリからソースファイルを探します。
PEAR の命名規則に従い、Project_Package_Class
クラスがソースファイル Project/Package/Class.php
に対応します。
--repeat指定された回数だけ、繰り返しテストを実行します。
--tapTest Anything Protocol (TAP) を使用して、テストの進行状況を報告します。 詳細は 第 15 章 を参照ください。
--verboseより詳細な情報を出力します。例えば、 未完成のテストや省略したテストの名前が表示されます。
--wait各テストが終了するたびにキー入力待ちの状態になります。これは、 テストランナーが終了すると同時にウィンドウが閉じてしまうような場合に便利です。
--skeleton
(UnitTest.php に記述された) Unit
クラスに対して、テストケースクラス UnitTest
の雛形を記述したファイル UnitTest.php を作成します。
詳細は 第 16 章 を参照ください。
-d指定した PHP 設定オプションの値を設定します。
PHPUnit の目指すところ (第 2 章 を参照ください) のひとつに 「自由に組み合わせられる」ということがあります。つまり、 例えば「そのプロジェクトのすべてのテストを実行する」「プロジェクトの中の ある部品を構成するすべてのクラスについて、すべてのテストを実行する」 「特定のひとつのクラスのテストのみを実行する」など、 数や組み合わせにとらわれずに好きなテストを一緒に実行できるということです。
PHPUnit フレームワークの PHPUnit_Framework_TestSuite
クラスを使用すると、複数のテストを階層構造でテストスイートにまとめることができます。
実際の例として、PHPUnit 自身のテストスイートを見てみましょう。
例 6.1
は Tests/AllTests.php の一部を抜粋したもの、そして
例 6.2
は Tests/Framework/AllTests.php の一部を抜粋したものです。
例 6.1: AllTests クラス
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Framework/AllTests.php';
// ...
class AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('PHPUnit');
$suite->addTest(Framework_AllTests::suite());
// ...
return $suite;
}
}
?>
例 6.2: Framework_AllTests クラス
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Framework/AssertTest.php';
// ...
class Framework_AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('PHPUnit Framework');
$suite->addTestSuite('Framework_AssertTest');
// ...
return $suite;
}
}
?>
Framework_AssertTest クラスは、
基底クラス PHPUnit_Framework_TestCase
を継承した標準的なテストケースクラスです。
phpunit AllTests を
Tests ディレクトリで実行すると、すべてのテストを実行します。
phpunit Framework_AllTests AllTests.php を
Tests/Framework ディレクトリで実行すると、
PHPUnit_Framework_* クラスのテストのみを実行します。
phpunit Framework_AssertTest AssertTest.php を
Tests/Framework ディレクトリで実行すると、
PHPUnit_Framework_Assert クラスのテストのみを実行します。
phpunit --filter testFail Framework_AssertTest AssertTest.php を
Tests/Framework ディレクトリで実行すると、
Framework_AssertTest クラスの
testFail という名前のテストのみを実行します。
テストを記述する際にいちばん時間を食うのは、テストを開始するための事前設定と テスト終了後の後始末の処理を書くことです。この事前設定は、テストの fixture と呼ばれます。
例 4.1
では、fixture は
$fixture という変数に格納された配列だけでした。
しかしたいていの場合は fixture はこれより複雑なものとなり、
それを準備するにはかなりの量のコードが必要です。本来のテストの内容が、
fixture を設定するためのコードの中に埋もれてしまうことになります。
この問題は、複数のテストで同じような fixture を設定する場合により顕著になります。
テストフレームワークの助けがなければ、
個々のテストのなかで同じような準備コードを繰り返し書くはめになってしまいます。
PHPUnit は、準備用のコードの共有をサポートしています。
各テストメソッドが実行される前に、setUp()
という名前のテンプレートメソッドが実行されます。setUp()
は、テスト対象のオブジェクトを生成するような処理に使用します。
テストメソッドの実行が終了すると、それが成功したか否かにかかわらず、
tearDown() という名前の別のテンプレートメソッドが実行されます。
tearDown() では、テスト対象のオブジェクトの後始末などを行います。
それでは、setUp() を使用してコードの重複を排除するように
例 4.1
を書き換えてみましょう。
まず最初にインスタンス変数 $fixture を宣言し、
メソッド内のローカル変数ではなくこちらを使用するようにします。
そして、array fixture の生成処理を
setUp() メソッドに移動します。最後に、
テストメソッド内で重複しているコードを取り除き、
新しく作成したインスタンス変数を使用するようにします。つまり、
assertEquals() で使用しているローカル変数
$fixture を、$this->fixture
に置き換えます。
例 7.1: setUp() を使用して Array fixture を作成する
<?php
require_once 'PHPUnit/Framework.php';
class ArrayTest extends PHPUnit_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() が一度ずつ実行されるだけでなく、
テストメソッドごとに、新しいテストケースクラスのインスタンスが作成されます
(第 19 章 を参照ください)。
setUp() と tearDown()
は理屈上では対称的になるはずですが、実際にはそうではありません。実際には、
tearDown() を実装する必要があるのは setUp()
で外部リソース (ファイルやソケットなど) を割り当てた場合のみです。もし
setUp() で単に PHP オブジェクトを作成しただけの場合は、
一般には tearDown() は必要ありません。しかし、もし
setUp() で大量のオブジェクトを作成した場合には、
それらの後始末をするために tearDown() で変数を
unset() したくなることもあるでしょう。
テストケースオブジェクト自体のガベージコレクションにはあまり意味がありません。
ふたつのテストがあって、それぞれの setup がほんの少しだけ違う場合にはどうなるでしょう? このような場合は、二種類の可能性が考えられます。
もし setUp() の違いがごくわずかなものなら、
その違う部分を setUp()
からテストメソッドのほうに移動させます。
setUp() の違いが大きければ、
テストケースクラスを別に分ける必要があります。それぞれのクラスには、
setup の違いを表す名前をつけます。
PHPUnit には、スイートレベルでの設定をするための便利な方法はありません。 複数のテストの間で fixture を共有したいなんてことは、通常はめったにないはずです。 しかし、設計上の問題などでどうしても fixture を共有しなければならないこともあるでしょう。
複数のテスト間で共有する意味のある fixture の例として意味のあるものといえば、
データベースとの接続でしょう。テストのたびに新しいデータベース接続を毎回作成するのではなく、
最初にログインした状態を再利用するということです。こうすることで、
テストの実行時間を短縮できます。これを行うには、データベースに関するテストを
DatabaseTests という名前のクラスに書き、デコレータ
(decorator) オブジェクト TestSetup
でテストスイートをラップします。オーバーライドした setUp()
でデータベース接続をオープンし、 tearDown()
で接続を閉じるようにします。この例を
例 7.2
に示します。DatabaseTestSetup デコレータを起動することで、
DatabaseTests のテストを行うことができます。例えば、
PHPUnit のコマンドライン版テストランナーでは
phpunit DatabaseTestSetup とします。
例 7.2: スイートレベル設定のデコレータを書く
<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Extensions/TestSetup.php';
class DatabaseTestSetup extends PHPUnit_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 PHPUnit_Framework_TestSuite('DatabaseTests')
);
}
}
?>
このように fixture を共有することがテストの価値を下げてしまうということを、 まだうまく伝え切れていないかもしれません。問題なのは、 各オブジェクトが疎結合になっていないという設計なのです。 複数が連携しているようなテストを作って設計上の問題から目をそらしてしまうのではなく、 きちんと設計しなおした上で、スタブ (第 10 章 を参照ください) を使用するテストを書くことをお勧めします。
PHPUnit では、テストクラスの基底クラスである
PHPUnit_Framework_TestCase を拡張するための方法を提供しています。
これにより、例外や出力内容、そしてパフォーマンス低下のテストができるようになります。
PHPUnit_Extensions_ExceptionTestCase
を継承したテストクラスを作成し、
テストコードの内部で例外がスローされたかどうかを調べることもできます。
例 8.1
では、PHPUnit_Extensions_ExceptionTestCase
のサブクラスを作成し、テストしたい例外をその setExpectedException()
メソッドに設定する方法を示します。期待した例外がスローされなかった場合は、
そのテストは失敗という扱いになります。
例 8.1: PHPUnit_Extensions_ExceptionTestCase の使用法
<?php
require_once 'PHPUnit/Extensions/ExceptionTestCase.php';
class ExceptionTest extends PHPUnit_Extensions_ExceptionTestCase
{
public function testException()
{
$this->setExpectedException('Exception');
}
}
?>
phpunit ExceptionTest
PHPUnit 3.0.0 by Sebastian Bergmann.
F
Time: 00:00
There was 1 failure:
1) testException(ExceptionTest)
Expected exception Exception
FAILURES!
Tests: 1, Failures: 1.
表 8.1
は、PHPUnit_Extensions_ExceptionTestCase
が実装しているメソッドをまとめたものです。
表8.1 ExceptionTestCase
| メソッド | 意味 |
|---|---|
void setExpectedException(string $exceptionName) | 発生することを期待する例外の名前を $exceptionName に設定します。 |
String getExpectedException() | 発生することを期待する例外の名前を返します。 |
一方、 例 8.2 のような方法で例外をテストすることもできます。
例 8.2: 例外をテストするための、別の方法
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase {
public function testException() {
try {
// ... 例外が発生するであろうコード ...
}
catch (Exception $expected) {
return;
}
$this->fail('期待通りの例外が発生しませんでした。');
}
}
?>
例外が発生するはずの
例 8.2
のコードで例外が発生しなかった場合、それに続く
fail() (表 20.3 を参照ください)
によってテストが終了し、問題を報告します。期待通りに例外が発生すると、
catch ブロックが実行されてテストは正常終了します。
メソッドの実行結果を確かめる方法として、(echo や
print などによる)
出力が期待通りのものかを調べたいこともあるでしょう。
PHPUnit_Extensions_OutputTestCase クラスは、PHP の
出力バッファリング 機能を使用してこの仕組みを提供します。
例 8.3
では、PHPUnit_Extensions_OutputTestCase
のサブクラスを作成し、期待する出力内容を expectOutputString()
メソッドで設定する方法を示します。
期待通りの出力が得られなかった場合は、そのテストは失敗という扱いになります。
例 8.3: PHPUnit_Extensions_OutputTestCase の使用法
<?php
require_once 'PHPUnit/Extensions/OutputTestCase.php';
class OutputTest extends PHPUnit_Extensions_OutputTestCase
{
public function testExpectFooActualFoo()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectFooActualBar()
{
$this->expectOutputString('foo');
print 'bar';
}
}
?>
phpunit OutputTest
PHPUnit 3.0.0 by Sebastian Bergmann.
.F
Time: 00:00
There was 1 failure:
1) testExpectFooActualBar(OutputTest)
Failed asserting that <string:bar> is equal to <string:foo>.
FAILURES!
Tests: 2, Failures: 1.
表 8.2
PHPUnit_Extensions_OutputTestCase
が提供するメソッドをまとめたものです。
表8.2 OutputTestCase
| メソッド | 意味 |
|---|---|
void expectOutputRegex(string $regularExpression) | 出力が正規表現 $regularExpression にマッチするであろうという予測を設定します。 |
void expectOutputString(string $expectedString) | 出力が文字列 $expectedString と等しくなるであろうという予測を設定します。 |
PHPUnit_Extensions_PerformanceTestCase
を継承したテストクラスを使用すると、
関数やメソッドの実行が制限時間内に終わったかどうかなどをテストすることができます。
PHPUnit_Extensions_PerformanceTestCase
のサブクラスを作成してその setMaxRunningTime()
メソッドを使用し、実行時間の最大値を制限する方法を
例 8.4
で示します。
もしテストが制限時間内に終了しなければ、そのテストは失敗という扱いになります。
例 8.4: PHPUnit_Extensions_PerformanceTestCase の使用法
<?php
require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
class PerformanceTest extends PHPUnit_Extensions_PerformanceTestCase
{
public function testPerformance()
{
$this->setMaxRunningTime(2);
sleep(1);
}
}
?>
表 8.3
は、PHPUnit_Extensions_PerformanceTestCase
が実装しているメソッドをまとめたものです。
表8.3 PerformanceTestCase
| メソッド | 意味 |
|---|---|
void setMaxRunningTime(int $maxRunningTime) | テストの所要時間の最大値を (秒単位で) $maxRunningTime に設定します。 |
integer getMaxRunningTime() | このテストの最大所要時間を返します。 |
新しいテストケースクラスを作成する際には、 これから書くべきテストの内容をはっきりさせるために、 まず最初は以下のような空のテストメソッドを書きたくなることでしょう。
public function testSomething()
{
}
しかし、PHPUnit フレームワークでは空のメソッドを「成功した」
と判断してしまうという問題があります。このような解釈ミスがあると、
テスト結果のレポートが無意味になってしまいます。
そのテストがほんとうに成功したのか、
それともまだテストが実装されていないのかが判断できないからです。
実装していないテストメソッドの中で $this->fail()
をコールするようにしたところで事態は何も変わりません。
こうすると、テストが「失敗した」と判断されてしまいます。
これは未実装のテストが「成功」と判断されてしまうのと同じくらいまずいことです
(訳注: レポートを見ても、そのテストがほんとうに失敗したのか、
まだ実装されていないだけなのかがわかりません)。
テストの成功を青信号、失敗を赤信号と考えるなら、
テストが未完成あるいは未実装であることを表すための黄信号が必要です。
そのような場合に使用するインターフェイスが
PHPUnit_Framework_IncompleteTest で、
これは未完成あるいは未実装のテストメソッドで発生する例外を表すものです。
このインターフェイスの標準的な実装が
PHPUnit_Framework_IncompleteTestError です。
例 9.1
では SampleTest というテストケースクラスを定義しています。
便利なメソッド markTestIncomplete()
(これは、自動的に PHPUnit_Framework_IncompleteTestError
を発生させます) をテストメソッド内でコールすることで、
このメソッドがまだ完成していないことをはっきりさせます。
例 9.1: テストに未完成の印をつける
<?php
require_once 'PHPUnit/Framework.php';
class SampleTest extends PHPUnit_Framework_TestCase
{
public function testSomething()
{
// オプション: お望みなら、ここで何かのテストをしてください。
$this->assertTrue(TRUE, 'This should already work.');
// ここで処理を止め、テストが未完成であるという印をつけます。
$this->markTestIncomplete(
'このテストは、まだ実装されていません。'
);
}
}
?>
未完成のテストは、PHPUnit のコマンドライン版テストランナーでは以下のように
I で表されます。
phpunit --verbose SampleTest
PHPUnit 3.0.0 by Sebastian Bergmann.
I
Time: 00:00
There was 1 incomplete test:
1) testSomething(SampleTest)
This test has not been implemented yet.
/home/sb/SampleTest.php:12
OK, but incomplete or skipped tests!
Tests: 1, Incomplete: 1.表 9.1 に、テストを未完成扱いにするための API を示します。
表9.1 未完成のテスト用の API
| メソッド | 意味 |
|---|---|
void markTestIncomplete() | 現在のテストを未完成扱いにします。 |
void markTestIncomplete(string $message) | 現在のテストを未完成扱いにします。それを説明する文字列として $message を使用します。 |
すべてのテストがあらゆる環境で実行できるわけではありません。 考えてみましょう。たとえば、データベースの抽象化レイヤーを使用しており、 それがさまざまなドライバを使用してさまざまなデータベースシステムを サポートしているとします。MySQL ドライバのテストができるのは、 当然 MySQL サーバが使用できる環境だけです。
例 9.2
に示すテストケースクラス DatabaseTest には、
テストメソッド testConnection() が含まれています。
このクラスのテンプレートメソッド setUp() では、
MySQLi 拡張モジュールが使用可能かを調べたうえで、もし使用できない場合は
markTestSkipped() メソッドでテストを省略するようにしています。
例 9.2: テストを省略する
<?php
require_once 'PHPUnit/Framework.php';
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!extension_loaded('mysqli')) {
$this->markTestSkipped(
'MySQLi 拡張モジュールが使用できません。'
);
}
}
public function testConnection()
{
// ...
}
}
?>
飛ばされたテストは、PHPUnit のコマンドライン版テストランナーでは以下のように
S で表されます。
phpunit --verbose DatabaseTest
PHPUnit 3.0.0 by Sebastian Bergmann.
S
Time: 00:00
There was 1 skipped test:
1) testConnection(DatabaseTest)
The MySQLi extension is not available.
/home/sb/DatabaseTest.php:9
OK, but incomplete or skipped tests!
Tests: 1, Skipped: 1.表 9.2 に、テストを省略するための API を示します。
表9.2 テストを省略するための API
| メソッド | 意味 |
|---|---|
void markTestSkipped() | 現在のテストを省略扱いにします。 |
void markTestSkipped(string $message) | 現在のテストを省略扱いにします。それを説明する文字列として $message を使用します。 |
オブジェクトに対するコールが正しく行われたかどうかを調べたいこともあるでしょう。
その方法をここで説明します。ここでは、別のオブジェクトを観察している
あるオブジェクトの特定のメソッド (この例では update())
が正しくコールされたかどうかを調べるものとします。
例 10.1 では、まず
PHPUnit_Framework_TestCase クラスの
getMock() メソッド (表 20.7 を参照ください)
を使用して Observer のモックオブジェクトを作成します。
getMock() メソッドの二番目の (オプションの)
パラメータに配列を指定しているので、Observer
クラスの中の update() メソッドについてのみモック実装が作成されます。
次に、PHPUnit が提供する、いわゆる
Fluent Interface
(流れるようなインターフェイス)
を用いてモックの振る舞いや期待する動作を指定します。簡単に言うと、
いくつもの一時オブジェクト (例えば「update()
がコールされることを期待するオブジェクト」と「パラメータに
○○が指定されることを期待するオブジェクト」) を作成して、
期待値を設定したあとにそれらを連結するといった操作は必要ないということです。
その代わりに、例にあるようにメソッドの呼び出しを連結します。
このほうが、より読みやすく "流れるような" コードとなります。
例 10.1: あるメソッドが、指定したパラメータで一度だけコールされることを確かめるテスト
<?php
require_once 'PHPUnit/Framework.php';
class ObserverTest extends PHPUnit_Framework_TestCase
{
public function testUpdateIsCalledOnce()
{
// Observer クラスのモックオブジェクトを作成します。
// 対象とするのは update() メソッドのみです。
$observer = $this->getMock('Observer', array('update'));
// update() メソッドが一度だけコールされ、その際の
// パラメータは文字列 'something' となる、
// ということを期待しています。
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// Subject オブジェクトを作成し、Observer オブジェクトの
// モックをアタッチします。
$subject = new Subject;
$subject->attach($observer);
// $subject オブジェクトの doSomething() メソッドをコールします。
// これは、Observer オブジェクトのモックの update() メソッドを、
// 文字列 'something' を引数としてコールすることを期待されています。
$subject->doSomething();
}
}
?>
表 10.1 は、モックメソッドの予定実行回数を表すために使用できる matcher の一覧です。
表10.1 Matchers
| Matcher | 意味 |
|---|---|
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() | 評価対象のメソッドがゼロ回以上実行された際にマッチするオブジェクトを返します。 |
PHPUnit_Framework_MockObject_Matcher_InvokedCount never() | 評価対象のメソッドが実行されなかった際にマッチするオブジェクトを返します。 |
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce() | 評価対象のメソッドが最低一回以上実行された際にマッチするオブジェクトを返します。 |
PHPUnit_Framework_MockObject_Matcher_InvokedCount once() | 評価対象のメソッドが一度だけ実行された際にマッチするオブジェクトを返します。 |
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) | 評価対象のメソッドが指定した回数だけ実行された際にマッチするオブジェクトを返します。 |
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) | 評価対象のメソッドが $index 回目に実行された際にマッチするオブジェクトを返します。 |
これらの matcher とともに使用する制約については、 表 20.2 で説明しています。
あるいは、Subject の実装をテストする際に
モックオブジェクトを使用せずに 自己シャント (Self Shunt) パターン
を適用する方法もあります。
これは、テストケース自身をスタブとして使用します。
自己シャントというのは医学用語で、薬を注入する場所を確保するため、
チューブを挿入して動脈からとった血液を静脈にもどすことを意味します。
まず、Observer を実装したテストケースクラスを作成します。
Observer は、Subject
を観察したいオブジェクトが実装しなければならないインターフェイスです。
class ObserverTest extends PHPUnit_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
オブジェクトを作成したことに注意してください。スタブを利用する際には、
このような設計をお勧めします。こうすると、
オブジェクト間の結合度が緩やかになり、再利用性が高まります。
自己シャントパターンになじみのない方には、このテストはわかりにくいかもしれません。 いったいここで何が起こっているんだ? テストケース自身がオブザーバになるだって? でも、お決まりのパターンをいったん身につけてしまえば、 これらのテストは簡単に理解できるようになります。 テストの内容をつかむために必要なことは、 すべてひとつのクラスの中に含まれているのですから。
いろいろな要因で失敗する可能性があるテストよりも、 単一の事項のみをテストするテストのほうが情報を得やすくなります。では、 できるだけ外部からの影響を受けないようなテストを作成するには どうすればいいのでしょうか? 単純なことです。 高価で面倒で頼りなくて遅くて複雑なリソースを、 テスト用に自動で生成されたスタブに置き換えればいいのです。たとえば、 複雑な計算結果を単に定数に置き換えたものを、テスト用に作成すればいいのです。
スタブを使用すると、高価な外部リソースを使用することによる問題を解決できます。
たとえばデータベース接続のようなリソースをテスト間で共有するには
PHPUnit_Extensions_TestSetup デコレータを使用すればいいのですが、
テストの目的を考えると、データベースを使用せずにすむのならばそのほうがずっとよいでしょう。
例 10.2 に、スタブメソッドの作成と返り値の設定の方法を示します。
例 10.2: メソッド呼び出しのスタブの作成
<?php
require_once 'PHPUnit/Framework.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
$stub = $this->getMock('SomeClass');
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// $stub->doSomething() をコールすると、'foo' を返します。
}
}
?>
表 10.2 は、スタブメソッドの返り値を設定する際に使用できるメソッドの一覧です。
表10.2 スタブの API
| メソッド | 意味 |
|---|---|
PHPUnit_Framework_MockObject_Stub_Return returnValue(mixed $value) | メソッドが実行された場合の返り値を $value に設定します。 |
PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls onConsecutiveCalls(mixed $value, ...) | メソッドが続けて実行された場合に返す値を設定します。 |
また、スタブを使用することで、よりよい設計を行うことができるようにもなります。
あちこちで使用されているリソースを単一の窓口 (façade : ファサード)
経由でアクセスするようにすることで、
それを簡単にスタブに置き換えられるようになります。例えば、
データベースへのアクセスのコードをそこらじゅうにちりばめるのではなく、
その代わりに IDatabase インターフェイスを実装した単一の
Database オブジェクトを使用するようにします。すると、
IDatabase を実装したスタブを作成することで、
それをテストに使用できるようになるのです。同時に、
テストを行う際にスタブデータベースを使用するか
本物のデータベースを使用するかを選択できるようになります。
つまり開発時にはローカル環境でテストし、
統合テスト時には実際のデータベースでテストするといったことができるようになるのです。
スタブ化しなければならない機能は、たいてい同一オブジェクト内で密結合しています。 この機能ををひとつの結合したインターフェイスにまとめることで、 システムのそれ以外の部分との結合を緩やかにすることができます。
You can always write more tests. However, you will quickly find that only a fraction of the tests you can imagine are actually useful. What you want is to write tests that fail even though you think they should work, or tests that succeed even though you think they should fail. Another way to think of it is in cost/benefit terms. You want to write tests that will pay you back with information. テストはいくらでも書くことができる。でも、じきにわかるだろうが、 きみが考えているテストの中で本当に有用なものはごくわずかだ。 本当に書かなきゃいけないのは、 これは動くだろうと考えているにもかかわらず失敗するテスト。それから、 これは失敗するだろうと考えているにもかかわらず実際は成功するテストだ。 あるいはコストと利益の観点から考えてみてもいいだろう。 きみに何らかの情報を返してくれるテストを書かないとね。 | ||
| --Erich Gamma | ||
開発中のソフトウェアの内部構造を変更し、 わかりやすく変更が簡単なものにする必要が出てきたときのことを考えましょう。 それによってソフトウェアの外部的な振る舞いが変わってしまってはいけません。 この、いわゆる リファクタリング (日本語) を安全に行うにあたり、テストスイートが非常に重要となります。 もしテストスイートがなければ、リファクタリングによってシステムを壊してしまっても あなたはそれに気づかないでしょう。
以下の条件が、あなたのプロジェクトのコードや設計を改善するための助けとなるでしょう。 また、単体テストを使用することで、リファクタリングによって振る舞いが変化していないこと・ エラーが発生していないことが確認できます。
すべての単体テストが正常に動作すること。
コードが設計指針を満たしていること。
コードに冗長性がないこと。
コードには最小限のクラスおよびメソッドのみが含まれていること。
システムに新しい機能を追加する際には、まず最初にテストを書きます。 そのテストがきちんと実行できるようになった時点で、開発は終了です。 この手法については、次の章で詳しく説明します。
不具合の報告を受けたら、すぐにでもそれを修正したいと思われることでしょう。 しかし、あせって修正しようとしても、経験上なかなかうまくいきません。 不具合を修正したつもりが新たな不具合を引き起こしていたなんてこともありがちですね。
はやる気持ちを抑えて、以下のようにしてみましょう。
不具合を再現できることを確認します。
不具合が発生する最小限のコードを見つけます。例えば、 もしおかしな数値が出力されるのなら、 その数値を計算しているオブジェクトが何なのかを探します。
その不具合のせいで今は失敗する (そして、不具合が修正されたら成功する) テストを書きます。
不具合を修正します。
不具合が再現する最小限のコードを見つける過程で、 不具合の原因がわかるかもしれません。テストを書くことによって、 不具合を真の意味で修正できる可能性が高まるでしょう。なぜなら、 テストを書くことで、将来同じ間違いをする可能性を減らせるからです。 これまでに書いたすべてのテストが、 不注意によって別の問題を発生させる可能性を減らすために役立っているのです。
Unit testing offers many advantages:
Overall, integrated unit testing makes the cost and risk of any individual change smaller. It will allow the project to make [...] major architectural improvements [...] quickly and confidently. 単体テストには、こんなに多くの利点がある。
まとめよう。単体テストをうまく組み込めば、 プログラムを変更する際の手間やリスクをより減らすことになるのだ。 It will allow the project to make [...] major architectural improvements [...] quickly and confidently. | ||
| --Benjamin Smedberg | ||
テストファーストプログラミング、 エクストリームプログラミング、 そして テスト駆動開発 などのソフトウェア開発方法論において、単体テストは非常に重要な位置を占めています。 また、構造上この手法に対応できない言語については 規約による設計 (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 'PHPUnit/Framework.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_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()
メソッドを
例 12.2
のように実装することです。
例 12.2: テスト testBalanceIsInitiallyZero() をクリアするために必要なコード
<?php
class BankAccount
{
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
}
?>
これで最初のテストはクリアすることになりましたが、 2 番目のテストには失敗します。なぜなら、 テストメソッド内でコールしているメソッドがまだ実装されていないからです。
phpunit BankAccountTest
PHPUnit 3.0.0 by Sebastian Bergmann.
.
Fatal error: Call to undefined method BankAccount::withdrawMoney()
ふたつめの規約のテストをクリアするには、withdrawMoney()、
depositMoney() および setBalance()
の各メソッドを
例 12.3
のように実装しなければなりません。これらのメソッドは、
規約に反するような引数でコールされた場合には InvalidArgumentException
を発生させるように実装しています。
例 12.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 3.0.0 by Sebastian Bergmann.
....
Time: 00:00
OK (4 tests)
別の方法としては、PHPUnit_Framework_Assert
クラスが提供する静的なアサーションメソッドを用いて、コード内に
「規約による設計」方式のアサーションを記述するというものもあります。
例 12.4
がその例です。これらのアサーションのいずれかに失敗すると、例外
PHPUnit_Framework_AssertionFailedError が発生します。
この方式を用いると、条件チェックのコードを減らすことができてテストが読みやすくなります。
ただ、プログラムの実行時にも PHPUnit が必要になってしまいます。
例 12.4: 「規約による設計」のアサーションを使用した BankAccount クラス
<?php
require_once 'PHPUnit/Framework.php';
class BankAccount
{
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
public function setBalance($balance)
{
PHPUnit_Framework_Assert::assertTrue($balance >= 0);
$this->balance = $balance;
}
public function depositMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
$this->balance += $amount;
}
public function withdrawMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
PHPUnit_Framework_Assert::assertTrue($this->balance >= $amount);
$this->balance -= $amount;
}
}
?>
規約を満たすための条件をテスト内に記述することで、「規約による設計」
方式で BankAccount クラスをプログラミングしてきました。
次に、テストファーストプログラミングの考え方にしたがって、
テストをクリアするために必要なコードを記述してきました。
でも、ひとつ忘れてしまったことがあります。それは、
setBalance()、depositMoney()
および withdrawMoney() に正当な値を指定した場合に、
正常に動作することを確かめるテストを書くことです。
自分が書いたテストが妥当なものなのか、
それで十分なのかを調べるためのテストが必要ですね。次の章では、そのための
「コードカバレッジ解析」について説明します。
ユニットテストでコードをテストする方法はわかりました。でも、 テストそのものをテストするにはどうしたらいいのでしょう? テストされていないコードを見つけるには? 言い換えれば、まだテストで カバーされていない部分を見つけるには? 完全にテストができたことをどうやって確認するの? これらのすべての疑問に対する答えとなるのが、コードカバレッジ解析という手法です。 コードカバレッジ解析を行うと、 コードのどの部分がテストされたのかを調べることができるようになります。
PHPUnit のコードカバレッジ解析では、Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。 ステートメントカバレッジというのは、たとえば 100 行のコードで構成されるメソッドがあった場合に、 もしテストで実際に実行されたのがそのうちの 75 行だけだったなら、 そのメソッドのコードカバレッジは 75 パーセントだと考えるということです。
それでは、例 12.3
の BankAccount クラスについての
コードカバレッジレポートを作成してみましょう。
phpunit --report ./report BankAccountTest
PHPUnit 3.0.0 by Sebastian Bergmann.
....
Time: 00:00
OK (4 tests)
Generating report, this may take a moment.図 13.1 は、コードカバレッジレポートの一部を抜粋したものです。 テスト時に実行された行は、緑色で強調表示されます。 実行可能なコードであるにもかかわらず実行されなかった行については赤色で強調表示されます。 また、"無意味なコード" についてはオレンジ色で強調表示されます。 行の左にある数字は、その行をカバーするテストの数を表します。
BankAccount のコードカバレッジレポートからわかることは、
setBalance()、depositMoney()
をコールするテストがまだ存在しないということ、
そして withdrawMoney()
に正しい値を指定した場合のテストも存在しないということです。
BankAccountTest クラスに追加すべきテストを
例 13.1
に示します。これによって、BankAccount
クラスのテストケースを完全に網羅できるようになります。
例 13.1: 完全なコードカバレッジを達成するために欠けているテスト
<?php
require_once 'PHPUnit/Framework.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_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());
}
}
?>
図 13.2 は、
テストを追加した後の setBalance() のコードカバレッジです。
自動テストに慣れてくると、 ほかの目的のためにもテストを使いたくなってくることでしょう。 ここではそんな例を説明します。
一般的に、エクストリームプログラミングのようなアジャイルプロセスを採用しているプロジェクトでは、 ドキュメントの内容が実際の設計やコードに追いついていないことが多いものです。 エクストリームプログラミングでは コードの共同所有 (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 クラス
(例 12.1
を参照ください) のアジャイルな文書の例です。
BankAccount - Balance is initially zero - Balance cannot become negative
また、--testdox-html BankAccountTest.htm
を使用することで、アジャイルな文書を HTML 形式で作成することもできます。
アジャイルな文書は、プロジェクト内であなたが作成しようとしている外部パッケージについて、 このように動作するであるという期待をまとめた文書にもなります。 外部のパッケージを使用するときには、 そのパッケージが期待通りに動作しなくなるというリスクに常にさらされています。 パッケージのバージョンアップにより知らないうちに挙動が変わってしまい、 あなたのコードが動作しなくなる可能性もあります。そのようなことを避けるため、 「このパッケージはこのように動作するはず」 ということを常にテストケースで記述しておくようにします。テストが成功すれば、 期待通りに動作していることがわかります。もし動作仕様をすべてテストで記述できているのなら、 外部パッケージが将来バージョンアップされたとしても何の心配もいりません。 テストをクリアしたということは、システムは期待通りに動作するということだからです。
あるパッケージについての機能を文書化するためにテストを書いているとき、 そのテストの所有者はあなたです。今あなたがテストを作成しているパッケージの作者は、 そのテストのことについては何も知りません。パッケージの作者とよりつながりを深めるため、 作成したテストを使用してコミュニケートしたり、 そのテストを使用して共同作業をしたりすることができるでしょう。
あなたが作成したテストを使用してパッケージの作者と共同作業をすることになれば、 テストも共同で書くことになります。そうすることで、 より多くのテストケースを挙げられるようになるでしょう。 「暗黙の了解」などに頼っていては、共同作業はできません。 テストと同時に、あなたはそのパッケージに対して期待していることを正確に文書化することになります。 また、すべてのテストにクリアした時点で、 作者はパッケージが完成したことを知ることになります。
スタブ (本書の前のほうで説明した "モックオブジェクト" の章を参照ください) を使用することで、パッケージの作者と別れても作業できるようになります。 パッケージ作者の仕事は、パッケージの実際の実装でテストをクリアするようにすること。 そしてあなたの仕事はあなたが書いたコードでテストをクリアするようにすることです。 この段階になれば、あなたはスタブオブジェクトを使用すればよいのです。 このやり方により、2 つのチームが独立して開発できるようになります。
PHPUnit は、テスト結果の出力書式として以下のようなものに対応しています。
PHPUnit がサポートする XML のフォーマットは、
Apache Ant の JUnit タスク
が使用しているものを参考にしています。
以下の例は、ArrayTest のテストが生成した
XML ログファイルです。
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="ArrayTest"
file="/home/sb/ArrayTest.php"
tests="2"
failures="0"
errors="0"
time="0.016030">
<testcase name="testNewArrayIsEmpty"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="6"
time="0.008044"/>
<testcase name="testArrayContainsAnElement"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="15"
time="0.007986"/>
</testsuite>
</testsuites>
次の XML ログファイルは、テストクラス
FailureErrorTest にある 2 つのテスト
testFailure および testError
が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
tests="2"
failures="1"
errors="1"
time="0.019744">
<testcase name="testFailure"
class="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
line="6"
time="0.011456">
<failure type="PHPUnit_Framework_ExpectationFailedException">
testFailure(FailureErrorTest)
Failed asserting that <integer:2> matches expected value <integer:1>.
/home/sb/FailureErrorTest.php:8
</failure>
</testcase>
<testcase name="testError"
class="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
line="11"
time="0.008288">
<error type="Exception">testError(FailureErrorTest)
Exception:
/home/sb/FailureErrorTest.php:13
</error>
</testcase>
</testsuite>
</testsuites>
JavaScript Object Notation (JSON)
は、軽量なデータ交換用フォーマットです。次の例は、
ArrayTest のテストが作成した JSON メッセージです。
{"event":"suiteStart","suite":"ArrayTest","tests":2}
{"event":"test","suite":"ArrayTest",
"test":"testNewArrayIsEmpty(ArrayTest)","status":"pass",
"time":0.000460147858,"trace":[],"message":""}
{"event":"test","suite":"ArrayTest",
"test":"testArrayContainsAnElement(ArrayTest)","status":"pass",
"time":0.000422954559,"trace":[],"message":""}
次の JSON メッセージは、
FailureErrorTest にある 2 つのテスト
testFailure および testError
が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。
{"event":"suiteStart","suite":"FailureErrorTest","tests":2}
{"event":"test","suite":"FailureErrorTest",
"test":"testFailure(FailureErrorTest)","status":"fail",
"time":0.000483989716,"trace":[],"message":""}
{"event":"test","suite":"FailureErrorTest",
"test":"testError(FailureErrorTest)","status":"error",
"time":0.000466108322,"trace":[],"message":""}
Test Anything Protocol (TAP)
は、Perl のモジュールをテストする際に使用する、
シンプルなテキストベースのインターフェイスです。
以下の例は、ArrayTest のテストが生成した
TAP ログファイルです。
# TestSuite "ArrayTest" started. ok 1 - testNewArrayIsEmpty(ArrayTest) ok 2 - testArrayContainsAnElement(ArrayTest) # TestSuite "ArrayTest" ended. 1..2
次の TAP ログファイルは、テストクラス
FailureErrorTest にあるメソッド
testFailure および testError
が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。
# TestSuite "FailureErrorTest" started. not ok 1 - Failure: testFailure(FailureErrorTest) not ok 2 - Error: testError(FailureErrorTest) # TestSuite "FailureErrorTest" ended. 1..2
PHPUnit は、テスト結果を図示することができます。 GraphViz ツール群を使用すると、これを画像などの便利な形式に変換することができます。
phpunit --log-graphviz ArrayTest.dot ArrayTest
PHPUnit 3.0.0 by Sebastian Bergmann.
..
Time: 00:00
OK (2 tests)
次の例は、ArrayTest のテストから生成された
(そしてカレントディレクトリの ArrayTest.dot
に保存された) GraphViz マークアップです。
digraph G {
graph [ overlap="scale", splines="true", sep=".1", fontsize="8" ];
"ArrayTest" [ color="green" ];
subgraph "cluster_ArrayTest" {
label="";
"testNewArrayIsEmpty" [ color="green" ];
"testArrayContainsAnElement" [ color="green" ];
}
"ArrayTest" -> "testNewArrayIsEmpty";
"ArrayTest" -> "testArrayContainsAnElement";
}
GraphViz ソフトウェア群に含まれるコマンドラインツール
dot を使用すると、
このマークアップから図を作成することができます。
dot -T png -o ArrayTest.png ArrayTest.dot図 15.1 は、上の GraphViz マークアップをもとに作成したテスト結果の図です。
成功したテストは緑の線で、失敗したりエラーが発生したものは赤い線で、 そして不完全だったり省略されたりしたテストは黄色い線で表示されます。 テストスイートなどの親ノードは、もしその子ノード (テスト) が成功しなかった場合は緑の線になりません。
既存のコードのテストを記述する際は、 以下のようなコードを何度となく繰り返し記述することになるでしょう。
public function testMethod()
{
}PHPUnit は、既存のクラスのコードを解析して テストケースクラスの雛形を作成することができます。
次の例は、Calculator という名前のクラス
(例 16.1 を参照ください)
用のテストクラスの雛形を作成する手順を示すものです。
phpunit --skeleton Calculator
PHPUnit 3.0.0 by Sebastian Bergmann.
Wrote test class skeleton for Calculator to CalculatorTest.php.もとのクラスの各メソッドについて、 出来上がったテストケースクラスのテストケースは不完全な状態 (第 9 章 を参照ください) です。
作成されたテストケースクラスを実行した結果を以下に示します。
phpunit --verbose CalculatorTest
PHPUnit 3.0.0 by Sebastian Bergmann.
I
Time: 00:00
There was 1 incomplete test:
1) testAdd(CalculatorTest)
This test has not been implemented yet.
/home/sb/CalculatorTest.php:55
OK, but incomplete or skipped tests!
Tests: 1, Incomplete: 1.
@assert アノテーションを
メソッドのコメント部で使用すると、
シンプルではあるけれど意味のあるテストを自動的に生成することができます。
これは不完全なテストケースではありません。
例 16.2
に例を示します。
例 16.2: @assert アノテーションをつけた Calculator クラス
<?php
class Calculator
{
/**
* @assert (0, 0) == 0
* @assert (0, 1) == 1
* @assert (1, 0) == 1
* @assert (1, 1) == 2
*/
public function add($a, $b)
{
return $a + $b;
}
}
?>
もとのクラスの各メソッドについて、
@assert アノテーションの内容をチェックします。
これらは、以下のようなテストコードに変換されます。
/**
* Generated from @assert (0, 0) == 0.
*/
public function testAdd() {
$o = new Calculator;
$this->assertEquals(0, $o->add(0, 0));
}
作成されたテストケースクラスを実行した結果を以下に示します。
phpunit CalculatorTest
PHPUnit 3.0.0 by Sebastian Bergmann.
....
Time: 00:00
OK (4 tests)
表 16.1
に、サポートされる @assert
の種類と、それがどのようなテストコードに変換されるかをまとめました。
表16.1 サポートされる @assert アノテーション
| アノテーション | 変換後の内容 |
|---|---|
@assert (...) == X | assertEquals(X, method(...)) |
@assert (...) != X | assertNotEquals(X, method(...)) |
@assert (...) === X | assertSame(X, method(...)) |
@assert (...) !== X | assertNotSame(X, method(...)) |
Selenium RC はテストツールのひとつです。これを使用すると、 ウェブアプリケーションのユーザインターフェイスについてのテストを自動化することができます。 あらゆるプログラミング言語で稼動しているウェブサイトに対応しており、 現在主流のあらゆるブラウザで使用することができます。Selenium RC は Selenium Core を使用しています。これは、ブラウザ上でのタスクを自動的に実行する JavaScript のライブラリです。Selenium でのテストは、 一般のユーザが使用するのと同じようにブラウザ上で直接実行されます。 主な使用例としては、受け入れテスト (各システム単体のテストではなく、結合されたシステム全体に対するテスト) や ブラウザの互換性のテスト (ウェブアプリケーションを、さまざまなオペレーティングシステムやブラウザでテストする) などがあります。
Selenium RC のインストール手順は、次のようになります。
Selenium RC の nightly snapshot をダウンロードする。
現時点の正式リリース版 (0.9.0) は、たとえば Mozilla Firefox 2 などに対応していません。
server/selenium-server.jar を /usr/local/bin などにコピーする。java -jar /usr/local/bin/selenium-server.jar などのようにして Selenium RC サーバを起動する。これで、クライアント/サーバ プロトコルを用いて Selenium RC サーバにコマンドを送信できるようになりました。
PHPUnit_Extensions_SeleniumTestCase
は、Selenium RC と通信するためのプロトコルを実装したものです。
また、ウェブのテスト用に特化したアサーションメソッドも提供します。
例 17.1 は、
ウェブサイト http://www.example.com/
の <title> 要素の内容をテストする方法を示したものです。
例 17.1: PHPUnit_Extensions_SeleniumTestCase の使用例
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitleEquals('Example Web Page');
}
}
?>
PHPUnit_Framework_TestCase クラスとは異なり、
PHPUnit_Extensions_SeleniumTestCase を継承したテストケースクラスは
setUp() メソッドが必須となります。
このメソッド内で、Selenium RC セッションの設定を行います。
ここで使用できるメソッドの一覧は
表 17.1
を参照ください。
表17.1 Selenium RC API: セットアップ
| メソッド | 意味 |
|---|---|
void setBrowser(string $browser) | Selenium RC サーバが使用するブラウザを設定します。 |
void setBrowserUrl(string $browserUrl) | テストするベース URL を設定します。 |
void setHost(string $host) | Selenium RC サーバに接続する際のホスト名を設定します。 |
void setPort(int $port) | Selenium RC サーバに接続する際のポートを設定します。 |
void setTimeout(int $timeout) | Selenium RC サーバに接続する際のタイムアウト値を設定します。 |
表 17.2
は、PHPUnit_Extensions_SeleniumTestCase
が提供するさまざまなアサーションメソッドの一覧です。
表17.2 アサーション
| アサーション | 意味 |
|---|---|
void assertAlertPresent() | alert が発生していない場合にエラーを報告します。 |
void assertNoAlertPresent() | alert が発生している場合にエラーを報告します。 |
void assertChecked(string $locator) | $locator で表される要素がチェックされていない場合にエラーを報告します。 |
void assertNotChecked(string $locator) | $locator で表される要素がチェックされている場合にエラーを報告します。 |
void assertConfirmationPresent() | 確認ダイアログが表示されていない場合にエラーを報告します。 |
void assertNoConfirmationPresent() | 確認ダイアログが表示されている場合にエラーを報告します。 |
void assertEditable(string $locator) | $locator で表される要素が編集可能でない場合にエラーを報告します。 |
void assertNotEditable(string $locator) | $locator で表される要素が編集可能な場合にエラーを報告します。 |
void assertElementValueEquals(string $locator, string $text) | $locator で表される要素の値が $text と異なる場合にエラーを報告します。 |
void assertElementValueNotEquals(string $locator, string $text) | $locator で表される要素の値が $text と等しい場合にエラーを報告します。 |
void assertElementContainsText(string $locator, string $text) | $locator で表される要素が $text を含まない場合にエラーを報告します。 |
void assertElementNotContainsText(string $locator, string $text) | $locator で表される要素が $text を含む場合にエラーを報告します。 |
void assertElementPresent(string $locator) | $locator で表される要素が存在しない場合にエラーを報告します。 |
void assertElementNotPresent(string $locator) | $locator で表される要素が存在する場合にエラーを報告します。 |
void assertLocationEquals(string $location) | 現在の位置が $location と異なる場合にエラーを報告します。 |
void assertLocationNotEquals(string $location) | 現在の位置が $location と等しい場合にエラーを報告します。 |
void assertPromptPresent() | プロンプトが表示されていない場合にエラーを報告します。 |
void assertNoPromptPresent() | プロンプトが表示されている場合にエラーを報告します。 |
void assertSomethingSelected(string $selectLocator) | $selectLocator で表される項目が選択されていない場合にエラーを報告します。 |
void assertNothingSelected(string $selectLocator) | $selectLocator で表される項目が選択されている場合にエラーを報告します。 |
void assertTextPresent(string $pattern) | 指定したパターン $pattern が存在しない場合にエラーを報告します。 |
void assertTextNotPresent(string $pattern) | 指定したパターン $pattern が存在する場合にエラーを報告します。 |
void assertTitleEquals(string $title) | 現在のタイトルが $title と異なる場合にエラーを報告します。 |
void assertTitleNotEquals(string $title) | 現在のタイトルが $title と等しい場合にエラーを報告します。 |
void assertVisible(string $locator) | $locator で表される要素が不可視な場合にエラーを報告します。 |
void assertNotVisible(string $locator) | $locator で表される要素が可視の場合にエラーを報告します。 |
使用できるコマンドのリファレンスや実際の使用法については Selenium Core のドキュメント を参照ください。
Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily, leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly. 継続的インテグレーション というソフトウェア開発手法は、チームのメンバーどうしが お互いの開発内容を頻繁に結合させるというものだ、通常は、 最低でも一日に一度、あるいは場合によっては一日に何度もこれを行う。 結合の際には自動ビルド (テストを含む) を行い、 エラーはできるだけ早い段階で検出する。 多くのチームが、この手法によって結合時の問題を激減させている。 また、ソフトウェアをより高速に開発できるようにもなっている。 | ||
| --Martin Fowler | ||
この章では、まず継続的インテグレーションという技法の概要について述べ、 それを PHPUnit にどのように適用するかを説明していきます。
継続的インテグレーションでは、テストを含めたビルド手順を 完全に自動化して何度でも実行できるようにしておく必要があります。 これは、一日に何度も実行されます。 各開発者は、この仕組みによって結合を行い、結合時の問題を減らします。 自動化を実現するには cronjob を使用します。このジョブでは、まずプロジェクトの ソースコードリポジトリ から定期的に最新版をチェックアウトし、 テストを実行し、その結果を利用しやすい形式で出力します。
ここで CruiseControl のような継続的ビルドプロセス用のフレームワークの出番です。 このフレームワークには、メールでの通知や Apache Ant との統合、 またさまざまなバージョン管理ツールとの統合のためのプラグインが用意されています。 またウェブインターフェイスも用意されており、 最新のビルドおよび以前のビルドについての詳細を見ることができます。
次の例では、CruiseControl が
/usr/local/cruisecontrol
にインストールされていることを想定しています。
cd /usr/local/cruisecontrolmkdir -p projects/BankAccount/build/logscd projects/BankAccountsvn co svn://svn.phpunit.de/phpunit/phpunit/branches/release/3.0/PHPUnit/Samples/BankAccount sourcebuild.xml ファイルを編集します。例 18.1: projects/BankAccount/build.xml
<project name="BankAccount" default="build" basedir=".">
<target name="checkout">
<exec dir="${basedir}/source/" executable="svn">
<arg line="up"/>
</exec>
</target>
<target name="test">
<exec dir="${basedir}/source" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/bankaccount.xml BankAccountTest"/>
</exec>
</target>
<target name="build" depends="checkout,test"/>
</project>cd /usr/local/cruisecontrolconfig.xml ファイルを編集します。例 18.2: config.xml
<cruisecontrol>
<project name="BankAccount">
<plugin
name="svnbootstrapper"
classname="net.sourceforge.cruisecontrol.bootstrappers.SVNBootstrapper"/>
<plugin
name="svn"
classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN"/>
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<bootstrappers>
<svnbootstrapper localWorkingCopy="projects/${project.name}/source/"/>
</bootstrappers>
<modificationset quietperiod="30">
<svn localWorkingCopy="projects/${project.name}/source/"/>
</modificationset>
<schedule interval="300">
<ant
anthome="apache-ant-1.7.0"
buildfile="projects/${project.name}/build.xml"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<currentbuildstatuspublisher
file="logs/${project.name}/buildstatus.txt"/>
<email
mailhost="localhost"
buildresultsurl="http://cruise.example.com/buildresults/${project.name}"
skipusers="true"
spamwhilebroken="true"
returnaddress="project@example.com">
<failure address="dev@lists.example.com" reportWhenFixed="true"/>
</email>
</publishers>
</project>
</cruisecontrol>./cruisecontrol.shhttp://localhost:8080/ をブラウザで開きます。Apache Maven は、ソフトウェアプロジェクトの管理や理解のためのツールです。 プロジェクト指向モデル (POM) にもとづいた Apache Maven は、 プロジェクトのビルドやレポート、ドキュメントの作成を共通の情報から行います。
PHPUnit の XML ログ機能 (「XML 形式」 を参照ください)
で作成した XML のログファイルは、テストスイート単位で個別の XML に分割しないと
Apache Maven の
surefire plugin で処理することができません。
このプラグインはプロジェクトのテストフェーズで用いるもので、
アプリケーションのユニットテストを実行します。
例 18.4
に、XML の分割を行うための XSLT スタイルシートを示します。
例 18.3
は pom.xml 設定ファイルの例です。
例 18.3: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- ... -->
<prerequisites>
<maven>2.0.7</maven>
</prerequisites>
<!-- ... -->
<build>
<!-- ... -->
<plugins>
<plugin>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-trax</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>net.sf.saxon</groupId>
<artifactId>saxon</artifactId>
<version>8.7</version>
</dependency>
</dependencies>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.2-SNAPSHOT</version>
<executions>
<execution>
<id>codecoverage</id>
<phase>pre-site</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<property name="phpunit.codecoverage"
location="${project.reporting.outputDirectory}/phpunit/codecoverage" />
<property name="surefire.reports"
location="${project.build.directory}/surefire-reports" />
<mkdir dir="${phpunit.codecoverage}"/>
<mkdir dir="${surefire.reports}"/>
<!-- ${ant.phpunit} path to PHPUnit executable -->
<!-- ${ant.pear};... this is the include path for your test execution -->
<!-- ${test.AllTests} PHPUnit cmd line args like 'AllTests de/dmc/dashboard/AllTests.php' -->
<exec executable="${ant.phpunit}" dir="${basedir}">
<arg line="-d include_path=${ant.pear};${project.build.sourceDirectory};${project.build.testSourceDirectory}
--report ${phpunit.codecoverage} ${test.AllTests}" />
</exec>
<xslt in="${phpunit.codecoverage}/logfile.xml"
out="${surefire.reports}/xslt.info"
style="src/test/config/phpunit_to_surefire.xslt"
processor="trax">
<!-- this is the output folder for surefire like XML Reports -->
<param name="outputDir" expression="${surefire.reports}"/>
</xslt>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.4-SNAPSHOT</version>
<reportSets>
<reportSet>
<reports>
<report>report-only</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
</project>例 18.4: phpunit_to_surefire.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="outputDir">.</xsl:param>
<xsl:template match="testsuites">
<xsl:apply-templates select="testsuite"/>
</xsl:template>
<xsl:template match="testsuite">
<xsl:if test="testcase">
<xsl:variable name="outputName" select="./@name"/>
<xsl:result-document href="file:///{$outputDir}/{$outputName}.xml" method="xml">
<xsl:copy-of select="."/>
</xsl:result-document>
</xsl:if>
<xsl:apply-templates select="testsuite"/>
</xsl:template>
</xsl:stylesheet>
PHPUnit の実装はちょっと見慣れないものでしょう。 通常のアプリケーションでは保守しづらくなるようなテクニックを使用したりしています。 PHPUnit がテストを実行する仕組みを知っておくと、 あなたがテストを書く際に役立つこともあるでしょう。
個々のテストは PHPUnit_Framework_Test
のオブジェクトで表され、テストを実行するには PHPUnit_Framework_TestResult
のオブジェクトが必要です。PHPUnit_Framework_TestResult
オブジェクトが PHPUnit_Framework_Test オブジェクトの
run() メソッドに渡され、
このメソッドが実際のテストメソッドを実行します。そこで発生した例外を
PHPUnit_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 がテストを実行するしくみをより深く探るため、 例 19.1 のようなテストクラスを考えてみましょう。
例 19.1: The EmptyTest class
<?php
require_once 'PHPUnit/Framework.php';
class EmptyTest extends PHPUnit_Framework_TestCase
{
private $emptyArray = array();
public function testSize()
{
$this->assertEquals(0, sizeof($this->emptyArray));
}
public function testIsEmpty()
{
$this->assertTrue(empty($this->emptyArray));
}
}
?>
テストが実行されるときに PHPUnit がまず行うのは、テストクラスを
PHPUnit_Framework_Test オブジェクトに変換することです。
ここでは、PHPUnit_Framework_TestSuite には
図 19.1
に見られるように 2 つの EmptyTest インスタンスが含まれます。
PHPUnit_Framework_TestSuite の実行時には、各
EmptyTest が順に実行されます。その中では各自の
setUp() メソッドが実行され、各テストについて
図 19.2
に見られるような新しい $emptyArray を作成します。
こうすることで、あるテストが配列を変更したとしても
それが他のテストに影響を及ぼさないようになります。
仮にグローバル変数やスーパーグローバル変数 ($_ENV など)
を変更したとしても、それは他のテストには影響を及ぼしません。
つまり、テストが実行される際には、ひとつのテストケースクラスが
2 段階のオブジェクトツリーになるということです。各テストは setUp()
で作成された自分自身のコピーの上で実行され、テストは完全に独立して実行されます。
PHPUnit は、リフレクションを使用してインスタンス変数 $name
からメソッド名を取得し、そのテストメソッドを実行します。これは、Smalltalk
の世界では Pluggable Selector と呼ばれているお決まりのパターンです。
Pluggable Selector を使用することでテストをよりシンプルに書くことができますが、
その代わりコードを見ただけではどのメソッドが実行されるのかがわからなくなります。
実行されるメソッドを知るには、実行時のデータの値を調べなければならないのです。
たいていの場合は、PHPUnit の API は単純なものです。単に
PHPUnit_Framework_TestCase を継承したテストケースを作成し、
assertTrue() あるいは assertEquals()
をコールすればよいのです。しかし、PHPUnit をより深く知りたい方のために、
ここではすべてのクラスおよび公開メソッドを説明します。
ほとんどの場合、PHPUnit を使用する際には以下の 5 つのクラスやインターフェイスに出会うことになるでしょう。
PHPUnit_Framework_Assert実際の値が想定した値どおりかどうかを調べるための静的メソッドを集めたもの。
PHPUnit_Framework_Testテストケースとして動作するすべてのオブジェクトのインターフェイス。
PHPUnit_Framework_TestCaseひとつのテスト。
PHPUnit_Framework_TestSuiteテストの集まり。
PHPUnit_Framework_TestResultひとつあるいは複数のテストの実行結果をまとめたもの。
PHPUnit の、5 つの基本クラス/インターフェイスである
PHPUnit_Framework_Assert、
PHPUnit_Framework_Test、
PHPUnit_Framework_TestCase、
PHPUnit_Framework_TestSuite およびand
PHPUnit_Framework_TestResult の関係を
図 20.1
に示します。
PHPUnit 用に書かれたテストケースのほとんどは、間接的に
PHPUnit_Framework_Assert
を継承しています。ここには、
値を自動的にチェックして矛盾を報告するためのメソッドが含まれています。
これらのメソッドは静的に宣言されているので、
あなたが作成したメソッドの中で「規約による設計」方式のアサーションを使用し、
PHPUnit に結果を報告させることができます
(例 20.1 を参照ください)。
例 20.1: 「規約による設計」方式のアサーション
<?php
require_once 'PHPUnit/Framework.php';
class Sample
{
public function aSampleMethod($object)
{
PHPUnit_Framework_Assert::assertNotNull($object);
}
}
$sample = new Sample;
$sample->aSampleMethod(NULL);
?>
Fatal error: Uncaught exception 'PHPUnit_Framework_ExpectationFailedException' with message 'Failed asserting that <null> is not identical to <null>'.
しかし、ほとんどの場合はこれらのアサーションはテストの中で行います。
各アサーションメソッドには 2 種類の方式があります。 エラー時に表示されるメッセージをパラメータとして指定する方法としない方法です。 オプションで指定したメッセージは、通常はテストが失敗したことが報告される場面で 表示されます。これにより、デバッグが楽になります。
例 20.2: メッセージつきのアサーション
<?php
require_once 'PHPUnit/Framework.php';
class MessageTest extends PHPUnit_Framework_TestCase
{
public function testMessage()
{
$this->assertTrue(FALSE, 'これは独自のメッセージです。');
}
}
?>
以下の例は、
例 20.2 のテスト
testMessage()
でメッセージつきのアサーションを使用した場合の出力結果です。
phpunit MessageTest.php
PHPUnit 3.0.0 by Sebastian Bergmann.
F
Time: 00:00
There was 1 failure:
1) testMessage(MessageTest)
これは独自のメッセージです。
Failed asserting that <boolean:false> is identical to <boolean:true>.
/home/sb/MessageTest.php:8
FAILURES!
Tests: 1, Failures: 1.表 20.1 に、すべてのアサーションをまとめます。
表20.1 アサーション
| アサーション | 意味 |
|---|---|
void assertTrue(bool $condition) | $condition が FALSE の場合にエラーを報告します。 |
void assertTrue(bool $condition, string $message) | $condition が FALSE の場合にエラー $message を報告します。 |
void assertFalse(bool $condition) | $condition が TRUE の場合にエラーを報告します。 |
void assertFalse(bool $condition, string $message) | $condition が TRUE の場合にエラー $message を報告します。 |
void assertNull(mixed $variable) | $variable が NULL でないときにエラーを報告します。 |
void assertNull(mixed $variable, string $message) | $variable が NULL でないときにエラー $message を報告します。 |
void assertNotNull(mixed $variable) | $variable が NULL の場合にエラーを報告します。 |
void assertNotNull(mixed $variable, string $message) | $variable が NULL の場合にエラー $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 assertAttributeSame(object $expected, string $actualAttributeName, object $actualObject) | $actualObject->actualAttributeName と $actual が同じオブジェクトを参照していない場合にエラーを報告します。 $actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeSame(object $expected, string $actualAttributeName, object $actualObject, string $message) | $actualObject->actualAttributeName と $actual が同じオブジェクトを参照していない場合にエラー $message を報告します。 $actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeSame(mixed $expected, string $actualAttributeName, object $actualObject) | $actualObject->actualAttributeName と $actual が同じ型、同じ値でない場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeSame(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | $actualObject->actualAttributeName と $actual が同じ型、同じ値でない場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotSame(object $expected, string $actualAttributeName, object $actualObject) | $actualObject->actualAttributeName と $actual が同じオブジェクトを参照している場合にエラーを報告します。 $actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotSame(object $expected, string $actualAttributeName, object $actualObject, string $message) | $actualObject->actualAttributeName と $actual が同じオブジェクトを参照している場合にエラー $message を報告します。 $actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, object $actualObject) | $actualObject->actualAttributeName と $actual が同じ型、同じ値である場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotSame(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | $actualObject->actualAttributeName と $actual が同じ型、同じ値である場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
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 assertAttributeEquals(array $expected, string $actualAttributeName, object $actualObject) | 2 つの配列 $expected と $actualObject->actualAttributeName が等しくない場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(array $expected, string $actualAttributeName, object $actualObject, string $message) | 2 つの配列 $expected と $actualObject->actualAttributeName が等しくない場合にエラーを $message 報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, object $actualObject) | 2 つの配列 $expected と $actualObject->actualAttributeName が等しい場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(array $expected, string $actualAttributeName, object $actualObject, string $message) | 2 つの配列 $expected と $actualObject->actualAttributeName が等しい場合にエラーを $message 報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(float $expected, string $actualAttributeName, object $actualObject, '', float $delta = 0) | 2 つの float 値 $expected と $actualObject->actualAttributeName の誤差が $delta より大きい場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(float $expected, string $actualAttributeName, object $actualObject, string $message, float $delta = 0) | 2 つの float 値 $expected と $actualObject->actualAttributeName の誤差が $delta より大きい場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, object $actualObject, '', float $delta = 0) | 2 つの float 値 $expected と $actualObject->actualAttributeName の誤差が $delta 以下の場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(float $expected, string $actualAttributeName, object $actualObject, string $message, float $delta = 0) | 2 つの float 値 $expected と $actualObject->actualAttributeName の誤差が $delta 以下の場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(string $expected, string $actualAttributeName, object $actualObject) | 2 つの文字列 $expected と $actualObject->actualAttributeName が等しくない場合にエラーを報告します。エラーは、2 つの文字列の差分で報告されます。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(string $expected, string $actualAttributeName, object $actualObject, string $message) | 2 つの文字列 $expected と $actualObject->actualAttributeName が等しくない場合にエラー $message を報告します。エラーは、2 つの文字列の差分で報告されます。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, object $actualObject) | 2 つの文字列 $expected と $actualObject->actualAttributeName が等しい場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(string $expected, string $actualAttributeName, object $actualObject, string $message) | 2 つの文字列 $expected と $actualObject->actualAttributeName が等しい場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, object $actualObject) | 2 つの変数 $expected と $actualObject->actualAttributeName が等しくない場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeEquals(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | 2 つの変数 $expected と $actualObject->actualAttributeName が等しくない場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, object $actualObject) | 2 つの変数 $expected と $actualObject->actualAttributeName が等しい場合にエラーを報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotEquals(mixed $expected, string $actualAttributeName, object $actualObject, string $message) | 2 つの変数 $expected と $actualObject->actualAttributeName が等しい場合にエラー $message を報告します。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
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 assertContains(string $needle, string $haystack) | $needle が $haystack の一部でない場合にエラーを報告します。 |
void assertContains(string $needle, string $haystack, string $message) | $needle が $haystack の一部でない場合にエラー $message を報告します。 |
void assertNotContains(string $needle, string $haystack) | $needle が $haystack の一部である場合にエラーを報告します。 |
void assertNotContains(string $needle, string $haystack, string $message) | $needle が $haystack の一部である場合にエラー $message を報告します。 |
void assertAttributeContains(mixed $needle, string $actualAttributeName, object $actualObject) | $needle が $actualObject->actualAttributeName の要素でない場合にエラーを報告します。$actualObject->actualAttributeName は配列、文字列あるいは Iterator インターフェイスを実装するオブジェクトのいずれかです。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeContains(mixed $needle, string $actualAttributeName, object $actualObject, string $message) | $needle が $actualObject->actualAttributeName の要素でない場合にエラー $message を報告します。$actualObject->actualAttributeName は配列、文字列あるいは Iterator インターフェイスを実装するオブジェクトのいずれかです。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotContains(mixed $needle, string $actualAttributeName, object $actualObject) | $needle が $actualObject->actualAttributeName の要素である場合にエラーを報告します。$actualObject->actualAttributeName は配列、文字列あるいは Iterator インターフェイスを実装するオブジェクトのいずれかです。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
void assertAttributeNotContains(mixed $needle, string $actualAttributeName, object $actualObject, string $message) | $needle が $actualObject->actualAttributeName の要素である場合にエラー $message を報告します。$actualObject->actualAttributeName は配列、文字列あるいは Iterator インターフェイスを実装するオブジェクトのいずれかです。$actualObject->actualAttributeName 属性は public、protected あるいは private のいずれかとなります。 |
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を報告します。 |
void assertFileExists(string $filename) | ファイル $filename が存在しない場合にエラーを報告します。 |
void assertFileExists(string $filename, string $message) | ファイル $filename が存在しない場合にエラー $message を報告します。 |
void assertFileNotExists(string $filename) | ファイル $filename が存在する場合にエラーを報告します。 |
void assertFileNotExists(string $filename, string $message) | ファイル $filename が存在する場合にエラー $message を報告します。 |
void assertObjectHasAttribute(string $attributeName, object $object) | $object->attributeName が存在しない場合にエラーを報告します。 |
void assertObjectHasAttribute(string $attributeName, object $object, string $message) | $object->attributeName が存在しない場合にエラー $message を報告します。 |
void assertObjectNotHasAttribute(string $attributeName, object $object) | $object->attributeName が存在する場合にエラーを報告します。 |
void assertObjectNotHasAttribute(string $attributeName, object $object, string $message) | $object->attributeName が存在する場合にエラー $message を報告します。 |
もっと複雑なアサーションを行う場合には、
PHPUnit_Framework_Constraint クラスを使用します。
これらは、assertThat() メソッドを使用して
例 20.3
のように評価されます。
例 20.3: assertThat() での制約オブジェクトの使用
<?php
require_once 'PHPUnit/Framework.php';
class ConstraintTest extends PHPUnit_Framework_TestCase
{
public function testNotEquals()
{
$constraint = $this->logicalNot(
$this->equalTo('foo')
);
$this->assertThat('foo', $constraint);
}
}
?>
<userinput>phpunit ConstraintTest</userinput> PHPUnit 3.0.0 by Sebastian Bergmann. F Time: 00:00 There was 1 failure: 1) testNotEquals(ConstraintTest) Failed asserting that <string:foo> is not equal to <string:foo>. /home/sb/ConstraintTest.php:12 FAILURES! Tests: 1, Failures: 1.
表 20.2 に、
使用できる PHPUnit_Framework_Constraint
の実装をまとめます。
表20.2 制約
| 制約 | 意味 |
|---|---|
PHPUnit_Framework_Constraint_IsAnything anything() | あらゆる入力値を受け入れる制約。 |
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $key) | 配列が指定したキーを保持していることを保証する制約。 |
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $value) | Iterator インターフェイスを実装している array やオブジェクトが、指定した値を保持していることを保証する制約。 |
PHPUnit_Framework_Constraint_IsEqual equalTo($value, $delta = 0, $maxDepth = 10) | ある値が別の値と等しいかどうかを調べる制約。 |
PHPUnit_Framework_Constraint_FileExists fileExists() | 指定した名前のファイルが存在するかどうかを調べる制約。 |
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $value) | 評価される値が、指定した値より大きいことを保証する制約。 |
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $attributeName) | 評価されるオブジェクトが、指定した属性を保持していることを保証する制約。 |
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $value) | ある値が別の値と同一であることを保証する制約。 |
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $className) | 評価されるオブジェクトが、指定したクラスのインスタンスであることを保証する制約。 |
PHPUnit_Framework_Constraint_IsType isType(string $type) | 評価される値が、指定した型であることを保証する制約。 |
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $value) | 評価される値が、指定した値より小さいことを保証する制約。 |
logicalAnd() | 論理積 (AND)。 |
logicalNot(PHPUnit_Framework_Constraint $constraint) | 論理否定 (NOT)。 |
logicalOr() | 論理和 (OR)。 |
logicalXor() | 排他的論理和 (XOR)。 |
PHPUnit_Framework_Constraint_PCREMatch matchesRegularExpression(string $pattern) | 評価される文字列が、正規表現にマッチすることを保証する制約。 |
PHPUnit_Framework_Constraint_StringContains stringContains(string $string, bool $case) | 評価される文字列が、指定した文字列を含むことを保証する制約。 |
これら以外に、プロジェクトで使用している
オブジェクト固有のアサーションが必要になることもあるでしょう。独自の
Assert クラスを作成し、
そこに独自のアサーションを含めてテストに使用することができます。
アサーションに失敗すると、ボトルネックメソッド
fail(string $message) がコールされ、これは
PHPUnit_Framework_AssertionFailedError をスローします。
このメソッドにもパラメータなしのものがあります。テストでエラーが発生した際に、
fail() を明示的にコールします。
例外が発生することが期待されるテストなどがその例になります。
表 20.3
に、PHPUnit のボトルネックメソッドをまとめます。
markTestIncomplete() および markTestSkipped()
は、テストに対して「未完了」あるいは「省略」の印をつけるために便利なメソッドです。
表20.4 テストに対して「未完了」あるいは「省略」の印をつける
| メソッド | 意味 |
|---|---|
void markTestIncomplete(string $message) | 現在のテストに「未完了」の印をつけます。$message はオプションです。 |
void markTestSkipped(string $message) | 現在のテストに「省略」の印をつけます。$message はオプションです。 |
単体テストとは、もともとクラスの公開インターフェイスをテストするものです。
しかし、時には非公開の属性の内容をテストしたいこともあるでしょう。
getAttribute() メソッドを使用すると、
指定したオブジェクトの属性の値を取得することができます。
表20.5 非公開属性へのアクセス
| メソッド | 意味 |
|---|---|
Mixed getAttribute($object, $attributeName) | オブジェクトの指定した属性 ($attributeName) の値を返します。protected あるいは private である属性についても動作します。 |
PHPUnit_Framework_Test は、
テストとして働くすべてのオブジェクトが使用する、
一般的なインターフェイスです。これを実装したオブジェクトは、
ひとつあるいは複数のテストを表すことになります。
表 20.6
に示す 2 つのメソッドが定義されています。
表20.6 実装することになるメソッド
| メソッド | 意味 |
|---|---|
int count() | テストの数を返します。 |
void run(PHPUnit_Framework_TestResult $result) | テストを実行し、結果を $result で報告します。 |
PHPUnit_Framework_Test の実装クラスとして有名なのは、
PHPUnit_Framework_TestCase および
PHPUnit_Framework_TestSuite の 2 つです。
PHPUnit_Framework_Test を実装したクラスを独自に作成することも可能です。
このインターフェイスはあえて小規模に設計されているので、実装するのは簡単でしょう。
テストケースクラスは PHPUnit_Framework_TestCase
クラスを継承して作成します。たいていの場合は、
テストスイートから自動的にテストを実行させることになるでしょう。
この場合、(規約により) 各テストは test*
という名前のメソッドにしておかなければなりません。
PHPUnit_Framework_TestCase は
PHPUnit_Framework_Test::countTestCases() を実装しており、
これは常に 1 を返します。このクラスで実装されている
PHPUnit_Framework_Test::run(PHPUnit_Framework_TestResult $result)
は、まず setUp() を実行し、テストメソッドを実行し、
それから tearDown() を実行し、その結果を
PHPUnit_Framework_TestResult に報告します。
PHPUnit_Framework_TestCase によって実装されているメソッドを
表 20.7 にまとめます。
表20.7 TestCase
| メソッド | 意味 |
|---|---|
__construct() | テストケースを作成します。 |
__construct(string $name) | 指定した名前のテストケースを作成します。この名前はテストケースを表示する際に使用されます。また、リフレクションで取得するテストメソッドの名前としても使用されます。 |
string getName() | テストケースの名前を返します。 |
void setName($name) | テストケースの名前を設定します。 |
PHPUnit_Framework_TestResult run(PHPUnit_Framework_TestResult $result) | テストケースを実行し、結果を $result に格納するための便利なメソッドです。 |
void runTest() | リフレクションによってテストメソッドを実行されたくない場合に、テストメソッドをオーバーライドします。 |
object getMock($className, [array $methods, [array $arguments, [string $mockClassName]]]) |
指定した $className 用のモックオブジェクト
(第 10 章 を参照ください) を返します。
デフォルトでは、していしたクラスの全メソッドのモックが作成されます。
二番目の (オプションの) パラメータを指定すると、
その配列の要素と一致する名前のメソッドについてのみモックが作成されます。
三番目の (オプションの) パラメータには、
モックオブジェクトのコンストラクタに渡すパラメータを配列で指定します。
四番目の (オプションの) パラメータを使用すると、
モックオブジェクトのクラス名を指定することができます。
|
void iniSet(string $varName, mixed $newValue) |
このメソッドは ini_set()
関数のラッパーです。テストが終了すると、php.ini
の設定を自動的にもとの値に戻します。
|
このクラスには 2 つのテンプレートメソッド setUp()
および tearDown() が存在します。これをオーバーライドすると、
実行しようとしているテストに関する前処理や後始末を行うことができます。
表 20.8
にこれらのメソッドをまとめます。
表20.8 テンプレートメソッド
| メソッド | 意味 |
|---|---|
void setUp() | これをオーバーライドして、実行するテストに関連するオブジェクトの作成を行います。テストケース内で各テストが実行されるたびに、setUp() が毎回コールされます。 |
void tearDown() | これをオーバーライドして、実行するテストに関連する、もう必要なくなったオブジェクトの後始末を行います。テストケース内で各テストが実行されるたびに、setUp() が毎回コールされます。一般に、tearDown() で明示的に後始末する必要があるのは外部リソース (例えばファイルやソケットなど) だけです。 |
PHPUnit_Framework_TestSuite は複数の
PHPUnit_Framework_Test を組み合わせたものです。
簡単に言うと、このクラスには複数のテストケースが含まれており、
テストスイートを実行するとそれらの全てのテストが実行されます。
テストスイートは composite なので、テストスイートの中に別のテストスイートを含め、
さらにそのテストスイートの中には別のテストスイートが含まれており……
といったことも可能です。これにより、
いろいろなところから集めたテストをひとまとめにすることが簡単になります。
run(PHPUnit_Framework_TestResult $result) および
countTestCases() の 2 つに加え、
PHPUnit_Framework_TestSuite は名前つきインスタンス、
名前なしインスタンスを作成するためのメソッドも用意しています。
PHPUnit_Framework_TestSuite
のインスタンスを作成するためのメソッドを
表 20.9
に示します。
表20.9 名前つき、あるいは名前なしインスタンスの作成
| メソッド | 意味 |
|---|---|
__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) | テストスイートの名前を設定します。 |
PHPUnit_Framework_TestSuite には、
PHPUnit_Framework_Test
を追加したり取得したりするためのメソッドも用意されています。これを
表 20.10
にまとめます。
表20.10 テストの追加、取得
| メソッド | 意味 |
|---|---|
void addTestSuite(PHPUnit_Framework_TestSuite $suite) | 別のテストスイートを、このテストスイートに追加します。 |
void addTestSuite(string $theClass) | test* という名前のテストメソッドを持つ $theClass という名前のクラスのインスタンスを含むテストスイートを、このテストスイートに追加します。 |
void addTestSuite(ReflectionClass $theClass) | test* という名前のテストメソッドを持つ $theClass で表されるクラスのインスタンスを含むテストスイートを、このテストスイートに追加します。 |
void addTest(PHPUnit_Framework_Test $test) | テストスイートに $test を追加します。 |
void addTestFile(string $filename) | 指定したソースファイルで定義されているクラスをテストスイートに追加します。 |
void addTestFiles(array $filenames) | 指定したソースファイルで定義されているクラスをテストスイートに追加します。 |
int testCount() | このテストスイートに直接登録されているテストの数を返します (再帰的には検索しません)。 |
PHPUnit_Framework_Test[] tests() | このテストスイートに直接登録されているテストを返します。 |
PHPUnit_Framework_Test testAt(int $index) | $index 番目のテストを返します。 |
例 20.4 に、テストスイートを作成して実行する方法を示します。
例 20.4: テストスイートの作成および実行
<?php
require_once 'PHPUnit/Framework.php';
require_once 'ArrayTest.php';
// ArrayTest クラスのテストを含む
// テストスイートを作成します。
$suite = new PHPUnit_Framework_TestSuite('ArrayTest');
// テストを実行します。
$suite->run();
?>
第 6 章 では、
PHPUnit_Framework_TestSuite クラスを使用して
階層化されたテストケースを組み合わせる例を示します。
これらのテストを実行している間は、実行したテストの数・失敗したテスト・
テストの所要時間などをどこかに保存しておかなければなりません。
これらの結果を収集するのが PHPUnit_Framework_TestResult
です。ひとつの PHPUnit_Framework_TestResult が、
テスト全体で使いまわされます。テストの実行結果や失敗の内容は
PHPUnit_Framework_TestResult に記録されていき、
実行が終了すると、PHPUnit_Framework_TestResult
には全てのテストの概要が含まれるようになります。
PHPUnit_Framework_TestResult は、
テストの進行状況を知りたい他のオブジェクトから参照されることもあります。
例えば、グラフィカルなテストランナーは PHPUnit_Framework_TestResult
を監視し、各テストの開始時にプログレスバーを更新するでしょう。
表 20.11 は、
PHPUnit_Framework_TestResult
の API をまとめたものです。
表20.11 TestResult
| メソッド | 意味 |
|---|---|
void addError(PHPUnit_Framework_Test $test, Exception $e) | 実行中の $test から予期せぬ $e がスローされたことを記録します。 |
void addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e) | 実行中の $test から予期せぬ $e がスローされたことを記録します。 |
PHPUnit_Framework_TestFailure[] errors() | 記録されたエラーを返します。 |
PHPUnit_Framework_TestFailure[] failures() | 記録された失敗を返します。 |
PHPUnit_Framework_TestFailure[] notImplemented() | 記録された未完了テストを返します。 |
int errorCount() | 記録されたエラーの数を返します。 |
int failureCount() | 記録された失敗の数を返します。 |
int notImplementedCount() | 未完了のテストケースの数を返します。 |
int count() | 実行したテストケースの総数を返します。 |
boolean wasSuccessful() | すべてのテストの実行に成功したかどうかを返します。 |
boolean allCompletlyImplemented() | すべてのテストが完全に実装されているかどうかを返します。 |
void collectCodeCoverageInformation(bool $flag) | コードカバレッジ情報の収集を有効あるいは無効にします。 |
array getCodeCoverageInformation() | 収集したコードカバレッジ情報を返します。 |
PHPUnit_Framework_TestResult
のオブザーバを登録したい場合は、PHPUnit_Framework_TestListener
を実装する必要があります。これを登録するには、
表 20.12
に示した addListener() を使用します。
表20.12 TestResult および TestListener
| メソッド | 意味 |
|---|---|
void addListener(PHPUnit_Framework_TestListener $listener) | $listener を登録し、テスト結果の内容が更新された場合にその内容を受け取るようにします。 |
void removeListener(PHPUnit_Framework_TestListener $listener) | 更新を受け取る $listener の登録を解除します。 |
表 20.13 に、テストリスナーが実装するメソッドを示します。 例 21.3 も参照ください。
表20.13 TestListener のコールバック
| メソッド | 意味 |
|---|---|
void addError(PHPUnit_Framework_Test $test, Exception $e) | $test が $e をスローしました。 |
void addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e) | $test がアサーションに失敗し、PHPUnit_Framework_AssertionFailedError 系がスローされました。 |
void addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e) | $test は完了しませんでした。 |
void addSkippedTest(PHPUnit_Framework_Test $test, Exception $e) | $test は実行されませんでした。 |
void startTestSuite(PHPUnit_Framework_TestSuite $suite) | $suite の実行が始まります。 |
void endTestSuite(PHPUnit_Framework_TestSuite $suite) | $suite の実行が終了しました。 |
void startTest(PHPUnit_Framework_Test $test) | $test の実行が始まります。 |
void endTest(PHPUnit_Framework_Test $test) | $test の実行が終了しました。 |
テストを書きやすくする、あるいはテストの実行結果の表示方法を変更するなど、 PHPUnit はさまざまな方法で拡張することができます。 PHPUnit を拡張するための第一歩をここで説明します。
PHPUnit_Framework_TestCase
を継承した抽象サブクラスにユーティリティメソッドを書き、
そのクラスをさらに継承してテストクラスを作成します。
これが、PHPUnit を拡張するための一番簡単な方法です。
PHPUnit_Extensions_TestDecorator
のサブクラスでテストケースあるいはテストスイートをラッピングし、
デコレータパターンを使用することで
各テストの実行前後に何らかの処理をさせることができます。
PHPUnit には、PHPUnit_Extensions_RepeatedTest
および PHPUnit_Extensions_TestSetup
という 2 つの具象テストデコレータが付属しています。
前者はテストを繰り返し実行し、それらが全て成功した場合にのみ成功とみなします。
後者については 第 7 章 で説明しました。
例 21.1
は、テストデコレータ PHPUnit_Extensions_RepeatedTest
の一部を抜粋したものです。独自のデコレータを作成するための参考にしてください。
例 21.1: RepeatedTest デコレータ
<?php
require_once 'PHPUnit/Extensions/TestDecorator.php';
class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
private $timesRepeat = 1;
public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
{
parent::__construct($test);
if (is_integer($timesRepeat) &&
$timesRepeat >= 0) {
$this->timesRepeat = $timesRepeat;
}
}
public function count()
{
return $this->timesRepeat * $this->test->count();
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = $this->createResult();
}
for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
$this->test->run($result);
}
return $result;
}
}
?>
PHPUnit_Framework_Test インターフェイスの機能は限られており、
実装するのは簡単です。PHPUnit_Framework_Test
を実装するのは PHPUnit_Framework_TestCase の実装より単純で、
これを用いて例えば データ駆動のテスト (data-driven tests)
などを実行します。
カンマ区切り (CSV) ファイルの値と比較する、データ駆動のテストを
例 21.2
に示します。このファイルの各行は foo;bar
のような形式になっており (訳注: CSV じゃない……)、
最初の値が期待値で 2 番目の値が実際の値です。
例 21.2: データ駆動のテスト
<?php
<programlisting><![CDATA[<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/Util/Timer.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
class DataDrivenTest implements PHPUnit_Framework_Test
{
private $lines;
public function __construct($dataFile)
{
$this->lines = file($dataFile);
}
public function count()
{
return 1;
}
public function run(PHPUnit_Framework_TestResult $result = NULL)
{
if ($result === NULL) {
$result = new PHPUnit_Framework_TestResult;
}
foreach ($this->lines as $line) {
$result->startTest($this);
PHPUnit_Util_Timer::start();
list($expected, $actual) = explode(';', $line);
try {
PHPUnit_Framework_Assert::assertEquals(trim($expected), trim($actual));
}
catch (PHPUnit_Framework_AssertionFailedError $e) {
$result->addFailure($this, $e, PHPUnit_Util_Timer::stop());
}
catch (Exception $e) {
$result->addError($this, $e, PHPUnit_Util_Timer::stop());
}
$result->endTest($this, PHPUnit_Util_Timer::stop());
}
return $result;
}
}
$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 3.0.0 by Sebastian Bergmann. .F Time: 0 seconds There was 1 failure: 1) DataDrivenTest Failed asserting that two strings are equal. expected string <bar> difference < x> got string <baz> /home/sb/DataDrivenTest.php:32 /home/sb/DataDrivenTest.php:53 FAILURES! Tests: 2, Failures: 1.
テスト結果をカスタマイズするために、必ず PHPUnit_Framework_TestResult
のサブクラスを書かなければならないというわけではありません。たいていは、
新しい PHPUnit_Framework_TestListener を実装して
(表 20.13 を参照ください)、
テストの前にそれを PHPUnit_Framework_TestResult
オブジェクトにアタッチするだけで十分です。
例 21.3
は、PHPUnit_Framework_TestListener
インターフェイスを実装する単純な例です。
例 21.3: シンプルなテストリスナー
<?php
require_once 'PHPUnit/Framework.php';
class SimpleTestListener
implements PHPUnit_Framework_TestListener
{
public function
addError(PHPUnit_Framework_Test $test,
Exception $e,
$time)
{
printf(
"テスト '%s' の実行中にエラーが発生しました。\n",
$test->getName()
);
}
public function
addFailure(PHPUnit_Framework_Test $test,
PHPUnit_Framework_AssertionFailedError $e,
$time)
{
printf(
"テスト '%s' に失敗しました。\n",
$test->getName()
);
}
public function
addIncompleteTest(PHPUnit_Framework_Test $test,
Exception $e,
$time)
{
printf(
"テスト '%s' は未完了です。\n",
$test->getName()
);
}
public function
addSkippedTest(PHPUnit_Framework_Test $test,
Exception $e,
$time)
{
printf(
"テスト '%s' は省略されました。\n",
$test->getName()
);
}
public function startTest(PHPUnit_Framework_Test $test)
{
printf(
"テスト '%s' が開始されました。\n",
$test->getName()
);
}
public function endTest(PHPUnit_Framework_Test $test, $time)
{
printf(
"テスト '%s' が終了しました。\n",
$test->getName()
);
}
public function
startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
printf(
"テストスイート '%s' が開始されました。\n",
$suite->getName()
);
}
public function
endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
printf(
"テストスイート '%s' が終了しました。\n",
$suite->getName()
);
}
}
?>
例 21.4 は、テストスイートを実行して監視する方法を示したものです。
例 21.4: テストスイートの実行と監視
<?php
require_once 'PHPUnit/Framework.php';
require_once 'ArrayTest.php';
require_once 'SimpleTestListener.php';
// ArrayTest クラスのテストを含む
// テストスイートを作成します。
$suite = new PHPUnit_Framework_TestSuite('ArrayTest');
// テスト結果オブジェクトを作成し、そこにオブザーバとして
// SimpleTestListener をアタッチします。
$result = new PHPUnit_Framework_TestResult;
$result->addListener(new SimpleTestListener);
// テストを実行します。
$suite->run($result);
?>
テストスイート 'ArrayTest' が開始されました。 テスト 'testNewArrayIsEmpty' が開始されました。 テスト 'testNewArrayIsEmpty' が終了しました。 テスト 'testArrayContainsAnElement' が開始されました。 テスト 'testArrayContainsAnElement' が終了しました。 テストスイート 'ArrayTest' が終了しました。
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 で作成されています。
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 3.0 Unported
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. TO THE EXTENT THIS
LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
SUCH TERMS AND CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the
Work and other pre-existing works, such as a translation,
adaptation, derivative work, arrangement of music or other
alterations of a literary or artistic work, or phonogram or
performance and includes cinematographic adaptations or any
other form in which the Work may be recast, transformed, or
adapted including in any form recognizably derived from the
original, except that a work that constitutes a Collection
will not be considered an Adaptation for the purpose of this
License. For the avoidance of doubt, where the Work is a
musical work, performance or phonogram, the synchronization of
the Work in timed-relation with a moving image ("synching")
will be considered an Adaptation for the purpose of this
License.
b. "Collection" means a collection of literary or artistic works,
such as encyclopedias and anthologies, or performances,
phonograms or broadcasts, or other works or subject matter
other than works listed in Section 1(f) below, which, by
reason of the selection and arrangement of their contents,
constitute intellectual creations, in which the Work is
included in its entirety in unmodified form along with one or
more other contributions, each constituting separate and
independent works in themselves, which together are assembled
into a collective whole. A work that constitutes a Collection
will not be considered an Adaptation (as defined above) for
the purposes of this License.
c. "Distribute" means to make available to the public the
original and copies of the Work or Adaptation, as appropriate,
through sale or other transfer of ownership.
d. "Licensor" means the individual, individuals, entity or
entities that offer(s) the Work under the terms of this License.
e. "Original Author" means, in the case of a literary or artistic
work, the individual, individuals, entity or entities who
created the Work or if no individual or entity can be
identified, the publisher; and in addition (i) in the case of
a performance the actors, singers, musicians, dancers, and
other persons who act, sing, deliver, declaim, play in,
interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the
producer being the person or legal entity who first fixes the
sounds of a performance or other sounds; and, (iii) in the
case of broadcasts, the organization that transmits the
broadcast.
f. "Work" means the literary and/or artistic work offered under
the terms of this License including without limitation any
production in the literary, scientific and artistic domain,
whatever may be the mode or form of its expression including
digital form, such as a book, pamphlet and other writing; a
lecture, address, sermon or other work of the same nature; a
dramatic or dramatico-musical work; a choreographic work or
entertainment in dumb show; a musical composition with or
without words; a cinematographic work to which are assimilated
works expressed by a process analogous to cinematography; a
work of drawing, painting, architecture, sculpture, engraving
or lithography; a photographic work to which are assimilated
works expressed by a process analogous to photography; a work
of applied art; an illustration, map, plan, sketch or three-
dimensional work relative to geography, topography,
architecture or science; a performance; a broadcast; a
phonogram; a compilation of data to the extent it is protected
as a copyrightable work; or a work performed by a variety or
circus performer to the extent it is not otherwise considered
a literary or artistic work.
g. "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.
h. "Publicly Perform" means to perform public recitations of the
Work and to communicate to the public those public
recitations, by any means or process, including by wire or
wireless means or public digital performances; to make
available to the public Works in such a way that members of
the public may access these Works from a place and at a place
individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of
the performances of the Work, including by public digital
performance; to broadcast and rebroadcast the Work by any
means including signs, sounds or images.
i. "Reproduce" means to make copies of the Work by any means
including without limitation by sound or visual recordings and
the right of fixation and reproducing fixations of the Work,
including storage of a protected performance or phonogram in
digital form or other electronic medium.
2. Fair Dealing Rights. Nothing in this License is intended to
reduce, limit, or restrict any uses free from copyright or rights
arising from limitations or exceptions that are provided for in
connection with the copyright protection 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 Collections, and to Reproduce the Work as incorporated
in the Collections;
b. to create and Reproduce Adaptations provided that any such
Adaptation, including any translation in any medium, takes
reasonable steps to clearly label, demarcate or otherwise
identify that changes were made to the original Work. For
example, a translation could be marked "The original work was
translated from English to Spanish," or a modification could
indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as
incorporated in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those
jurisdictions in which the right to collect royalties
through any statutory or compulsory licensing scheme cannot
be waived, the Licensor reserves the exclusive right to
collect such royalties for any exercise by You of the
rights granted under this License;
ii. Waivable Compulsory License Schemes. In those
jurisdictions in which the right to collect royalties
through any statutory or compulsory licensing scheme can
be waived, the Licensor waives the exclusive right to
collect such royalties for any exercise by You of the
rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right
to collect royalties, whether individually or, in the
event that the Licensor is a member of a collecting
society that administers voluntary licensing schemes, via
that society, from any exercise by You of the rights
granted under this License.
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. Subject to Section 8(f), 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 or Publicly Perform the Work only under the
terms of this License. You must include a copy of, or the
Uniform Resource Identifier (URI) for, this License with every
copy of the Work You Distribute or Publicly Perform. You may
not offer or impose any terms on the Work that restrict the
terms of this License or the ability of the recipient of the
Work to exercise the rights granted to that recipient under
the terms of the License. You may not sublicense the Work. You
must keep intact all notices that refer to this License and to
the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or
Publicly Perform the Work, You may not impose any effective
technological measures on the Work that restrict the ability
of a recipient of the Work from You to exercise the rights
granted to that recipient under the terms of the License. This
Section 4(a) applies to the Work as incorporated in a
Collection, but this does not require the Collection apart
from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any
Licensor You must, to the extent practicable, remove from the
Collection any credit as required by Section 4(b), as
requested. If You create an Adaptation, upon notice from any
Licensor You must, to the extent practicable, remove from the
Adaptation any credit as required by Section 4(b), as requested.
b. If You Distribute, or Publicly Perform the Work or any
Adaptations or Collections, You must, unless a request has
been made pursuant to Section 4(a), keep intact all copyright
notices for the Work and provide, reasonable to the medium or
means You are utilizing: (i) the name of the Original Author
(or pseudonym, if applicable) if supplied, and/or if the
Original Author and/or Licensor designate another party or
parties (e.g., a sponsor institute, publishing entity,
journal) for attribution ("Attribution Parties") in Licensor's
copyright notice, terms of service or by other reasonable
means, the name of such party or parties; (ii) the title of
the Work if supplied; (iii) to the extent reasonably
practicable, the URI, 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 (iv), consistent with Section 3(b), in the case of an
Adaptation, a credit identifying the use of the Work in the
Adaptation (e.g., "French translation of the Work by Original
Author," or "Screenplay based on original Work by Original
Author"). The credit required by this Section 4 (b) may be
implemented in any reasonable manner; provided, however, that
in the case of a Adaptation or Collection, at a minimum such
credit will appear, if a credit for all contributing authors
of the Adaptation or Collection appears, then as part of these
credits and in a manner at least as prominent as the credits
for the other contributing authors. For the avoidance of
doubt, You may only use the credit required by this Section
for the purpose of attribution in the manner set out above
and, by exercising Your rights under this License, You may not
implicitly or explicitly assert or imply any connection with,
sponsorship or endorsement by the Original Author, Licensor
and/or Attribution Parties, as appropriate, of You or Your use
of the Work, without the separate, express prior written
permission of the Original Author, Licensor and/or
Attribution Parties.
c. Except as otherwise agreed in writing by the Licensor or as
may be otherwise permitted by applicable law, if You
Reproduce, Distribute or Publicly Perform the Work either by
itself or as part of any Adaptations or Collections, You must
not distort, mutilate, modify or take other derogatory action
in relation to the Work which would be prejudicial to the
Original Author's honor or reputation. Licensor agrees that in
those jurisdictions (e.g. Japan), in which any exercise of the
right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion,
mutilation, modification or other derogatory action
prejudicial to the Original Author's honor and reputation, the
Licensor will waive or not assert, as appropriate, this
Section, to the fullest extent permitted by the applicable
national law, to enable You to reasonably exercise Your right
under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
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 Adaptations
or Collections 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 Perform the Work or a
Collection, 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 Perform an Adaptation,
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.
f. The rights granted under, and the subject matter referenced,
in this License were drafted utilizing the terminology of the
Berne Convention for the Protection of Literary and Artistic
Works (as amended on September 28, 1979), the Rome Convention
of 1961, the WIPO Copyright Treaty of 1996, the WIPO
Performances and Phonograms Treaty of 1996 and the Universal
Copyright Convention (as revised on July 24, 1971). These
rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be
enforced according to the corresponding provisions of the
implementation of those treaty provisions in the applicable
national law. If the standard suite of rights granted under
applicable copyright law includes additional rights not
granted under this License, such additional rights are deemed
to be included in the License; this License is not intended to
restrict the license of any rights under applicable law.
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, Creative Commons does not authorize
the use by either party of 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. For the avoidance of
doubt, this trademark restriction does not form part of this
License.
Creative Commons may be contacted at http://creativecommons.org/.
====================================================================