製作著作 © 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Sebastian Bergmann
PHPUnit 3.7 対応版 Updated on 2013-06-19.
assertArrayHasKey()assertClassHasAttribute()assertClassHasStaticAttribute()assertContains()assertContainsOnly()assertContainsOnlyInstancesOf()assertCount()assertEmpty()assertEqualXMLStructure()assertEquals()assertFalse()assertFileEquals()assertFileExists()assertGreaterThan()assertGreaterThanOrEqual()assertInstanceOf()assertInternalType()assertJsonFileEqualsJsonFile()assertJsonStringEqualsJsonFile()assertJsonStringEqualsJsonString()assertLessThan()assertLessThanOrEqual()assertNull()assertObjectHasAttribute()assertRegExp()assertStringMatchesFormat()assertStringMatchesFormatFile()assertSame()assertSelectCount()assertSelectEquals()assertSelectRegExp()assertStringEndsWith()assertStringEqualsFile()assertStringStartsWith()assertTag()assertThat()assertTrue()assertXmlFileEqualsXmlFile()assertXmlStringEqualsXmlFile()assertXmlStringEqualsXmlString()どんなにすぐれたプログラマも、間違いを犯します。 よいプログラマとそうでないプログラマの違いは、 よいプログラマはテストを行って間違いをできるだけ早く発見してしまうことです。 テストをするのが早ければ早いほど間違いを発見しやすくなり、 またそれを修正しやすくなります。 リリース直前までテストを先延ばしにしておくことが非常に問題であるのはこのためです。 そんなことをすると、すべてのエラーを発見しきることができず、 発見したエラーを修正することも非常に難しくなります。結局は、 トリアージを行ってどのエラーに対応するかを判断しなければならなくなります。 なぜならすべてのエラーを完全に修正することは不可能だからです。
PHPUnit を使用したテストは、全体としてはあなたがこれまでに行ってきたことと同じです。 ただ、そのやり方が違うだけです。それは、テスト つまりあなたのプログラムが期待通りにふるまうことを調べることと 総合テスト つまり実行可能なコード片がソフトウェアの各部分 (部品) を自動的にテストすることとの違いになります。実行可能なコード片のことを、 ユニットテスト (unit test) と呼びます。
この章では、単純な print
ベースのテストコードをもとにして完全な自動テストに書き換えていきます。
PHP 組み込みの array
をテストするように頼まれたとしましょう。このオブジェクトの機能のひとつに、
関数 count() があります。新しく作成された配列では、
count() 関数は 0 を返すはずです。
そして要素を 1 つ追加すると count() は
1 を返すようになるはずです。テストしたい内容を
例 1.1
に示します。
例 1.1: 配列操作のテスト
<?php
$fixture = array();
// $fixture は空のはずです。
$fixture[] = 'element';
// $fixture はひとつの要素を含むはずです。
?>
期待通りの結果が得られているかどうかを調べるためのいちばん単純な方法は、
要素を追加する前と後に count() の結果を表示することです
(例 1.2 を参照ください)。
それぞれ 0 および 1 が得られたら、
array および count()
が期待通りに動作していることになります。
例 1.2: print を使用した配列操作のテスト
<?php
$fixture = array();
print count($fixture) . "\n";
$fixture[] = 'element';
print count($fixture) . "\n";
?>
0 1
このテストは、成功したかどうかの判断を (出力結果を見て)
手動で行わなければなりません。今度は、この判断を自動でできるようにしてみましょう。
例 1.3 では、
期待される結果と実際の結果をコード中で比較して、もしそれらの値が等しければ
ok と表示します。もし not ok
と表示された場合は、どこかがおかしいということがわかります。
例 1.3: 期待値と実際の値を比較することによる配列操作のテスト
<?php
$fixture = array();
print count($fixture) == 0 ? "ok\n" : "not ok\n";
$fixture[] = 'element';
print count($fixture) == 1 ? "ok\n" : "not ok\n";
?>
ok ok
今度は、相違があった際に例外を発生させる関数を用意して、 期待値と実際の値を比較する処理を抽出してみましょう (例 1.4)。 これには 2 つの利点があります。テストが記述しやすくなること、 そして何か問題があったときにのみそれを出力させることができるということです。
例 1.4: アサーション関数を使用した配列操作のテスト
<?php
$fixture = array();
assertTrue(count($fixture) == 0);
$fixture[] = 'element';
assertTrue(count($fixture) == 1);
function assertTrue($condition)
{
if (!$condition) {
throw new Exception('Assertion failed.');
}
}
?>
これで、テストは完全に自動化されました。最初のバージョンでは単に テストする だけでしたが、このバージョンでは 自動テスト になっています。
自動テストを行う目的は、間違いを少なくすることです。 いくらすばらしいテストを行ったところで あなたのコードが完璧なものになるわけではありませんが、 自動テストを始めることで不具合の量を劇的に減らすことになるでしょう。 自動テストによってあなたのコードは信頼性の高いものとなり、大胆な設計変更 (リファクタリング) を行ったりチームメイトとの関係をよりよくしたり (複数チームでのテスト)、 その日の朝に比べて帰宅前のコードがよりよくなっていることを確信できたりといった効果があります。
いまのところ、組み込みの array および
count() 関数のテストしかありません。
PHP が提供する数多くの array_*()
関数をテストしようとすると、それらそれぞれについてテストを記述する必要があります。
それらのすべてのテストについて基盤部分を最初から書いていくこともできますが、
共通部分は一度だけ記述するようにし、
個々のテストではテスト固有の部分のみを記述していくほうがずっとよい方法です。
PHPUnit は、そのような基盤部分を提供します。
PHPUnit のようなフレームワークには、解決しなければならない制約があります。 その中のいくつかはお互いに相反するものです。 テストは、以下の条件を同時に満たす必要があります。
テストの書き方を身に着けるのが難しければ、 開発者はそんなものを覚えようとしないでしょう。
テストが書きにくければ、開発者はそんなものを書こうとしないでしょう。
テストコードには、外部からの要素を含めるべきではありません。 そうするとテストコードが周りのノイズに埋もれてしまいます。
ボタン一発でテストが実行でき、 その結果は明白な形式で表示されなければなりません。
一日に何百何千というテストを実行できるよう、 テストはすばやく実行できなければなりません。
テストは、お互い他のテストに影響を及ぼしてはいけません。 テストの実行順序を変えることでテストの結果が変わってしまってはいけません。
テストの数や組み合わせを自由に選択できなければなりません。 テストが独立している以上、これは当然のことです。
これらの制約の中には、相反する項目が 2 点あります。
一般に、テストにはプログラミング言語の全機能を必要としません。 多くのテストツールは、テストを記述するため必要最小限の機能のみを組み込んだ 独自のスクリプト言語を提供しています。その結果、テストは読みやすく、 また書きやすいものになります。 なぜならテストの内容から気をそらせるノイズがないためです。しかし、 またひとつ新たなプログラミング言語やプログラミングツールの使い方を覚える必要があり、 不便です。
ひとつのテストが他のテストの結果に何の影響もおよぼさないようにするには、 各テストの実行前に毎回テスト用の環境を構築し、 終了後には毎回それを元の状態に戻す必要があります。しかし、環境を構築する (例: データベースに接続し、特定の状態を表すデータを投入する) には長い時間がかかります。
PHPUnit は、テスト言語として PHP を使用することで これらの衝突を回避しようとしています。小規模で単純なテストを行う際には、 PHP の機能は行き過ぎた面もあるかもしれません。しかし、PHP を使用することで、 これまでの開発経験や開発ツールを武器として利用できます。 あまり気乗りしないテスターを納得させるため、 テストを最初に書き始める際の負担をできるだけ下げることが重要だと考えています。
PHPUnit では、実行速度よりもテストの独立性を重視しています。 独立したテストに価値があるのは、そのほうがより高品質のフィードバックが得られるからです。 一連のテストの最初のほうで失敗したことでその後のすべてのテストが失敗してしまい、 大量の失敗報告を受け取るようなことがなくなります。 このオリエンテーションでは独立したテストを目指し、 シンプルなオブジェクトを数多く作るという設計を心がけます。 各オブジェクトは独立してすばやくテストできます。結果としてよりよい設計 に加えてより高速なテストが可能となります。
PHPUnit ではほとんどのテストが成功することを想定しており、 成功したテストの詳細について報告することはあまり価値がないと考えています。 テストが失敗した場合には、そのことをしっかり報告しなければなりません。 大半のテストは成功すべきであり、 それらについては実行したテストの数以外に特にコメントすべき情報はありません。 これは、PHPUnit のコアではなく報告用のクラス群に組み込まれている機能です。 テストの結果が表示される際には実行したテストの数が表示されますが、 詳細が表示されるのは失敗したテストについてだけです。
テストは決め細やかに行うこと、つまりひとつのテストではひとつのオブジェクトの ひとつの機能についてをテストするようにすることが期待されています。 そのため、最初のテストは失敗し、テストの実行は終了し、 PHPUnit は失敗を報告します。多くの小さなテストを実行させることは一種の芸術です。 決め細やかなテストにより、システム全体の設計がよりよいものとなります。
PHPUnit でオブジェクトをテストする際には、 その公開インターフェースについてのみテストを行います。 公開されている振る舞いにのみ基づいてテストを行うことで、 設計上の困難な問題により早い段階で対応できるようになり、 設計ミスがシステムの大部分に影響を及ぼすことを避けられます。
PHPUnit は、三通りのインストール方法に対応しています。PEAR インストーラ あるいは Composer で、PHPUnit とその依存コンポーネントをダウンロードしてインストールすることもできるし、 PHPUnit を PHP Archive (PHAR) 形式でダウンロードすることもできます。PHAR 版の PHPUnit は、 必要な依存コンポーネントがすべて (オプションのコンポーネントの一部も含めて) ひとつのファイルにまとめられています。
Composer および PHP Archive (PHAR) には、PHPUnit 3.7 以降で対応するようになりました (安定したのは PHPUnit 3.7.5 からです)。 それより前のバージョンの PHPUnit は、これらの形式では配布されていません。
PHPUnit 3.7 は PHP 5.3.3 以降のバージョンで動作しますが、PHP 5.4.7 以降を使うことを強く推奨します。
PHPUnit では PHP_CodeCoverage というライブラリを使ってコードカバレッジ情報を処理していますが、 このライブラリを使うには Xdebug 2.0.5 以降が必要です。 しかし、Xdebug 2.2.0 以降を使うことを強く推奨します。
次のふたつのコマンドを実行するだけで (root として実行しなければならないかもしれません)、PHPUnit
に必要なものがすべて PEAR インストーラを使ってインストールできます。
pear config-set auto_discover 1pear install pear.phpunit.de/PHPUnit
OS のディストリビューションや PHP の環境によっては、 ここで説明する手順を実行する前に まず PEAR をインストールしたり既存の PEAR をアップデートしたりする必要があるかもしれません。
既存の PEAR 環境のアップデートは、通常は
sudo pear upgrade PEAR
を実行するだけのことです。PEAR の新規インストール方法は
PEAR
マニュアル に説明があります。
PHPUnit をプロジェクト単位で導入して開発用の依存関係を設定するには、
phpunit/phpunit への依存情報をプロジェクトの
composer.json ファイルに追加します。
次に示すのは最小限の
composer.json ファイルの例で、
開発時の PHPUnit 3.7 への依存を定義しています。
{
"require-dev": {
"phpunit/phpunit": "3.7.*"
}
}
システム全体で使えるようにスタンドアロンでインストールするには、
次のような composer.json を用意すればどのディレクトリからでもインストールできます。
{
"require": {
"phpunit/phpunit": "3.7.*"
},
"config": {
"bin-dir": "/usr/local/bin/"
}
}PHPUnit を PHP Archive (PHAR) 形式でダウンロードすることもできます。PHAR 版の PHPUnit は、 必要な依存コンポーネントがすべて (オプションのコンポーネントの一部も含めて) ひとつのファイルにまとめられています。
wget http://pear.phpunit.de/get/phpunit.pharchmod +x phpunit.phar
オプションのパッケージとして、これらが使えます。
DbUnit
DbUnit の PHP/PHPUnit 向けの移植。データベースとのやりとりをテスト可能にする。
このパッケージは、PEAR を使って次のコマンドでインストールします。
pear install phpunit/DbUnit
composer でインストールするには、
"require-dev"
に次の行を追加します。
"phpunit/dbunit": ">=1.2"PHP_Invoker
callable をタイムアウトつきで実行するユーティリティクラス。 テストのタイムアウトを厳格に指定するために必要なパッケージ。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHP_Invoker
composer でインストールするには、
"require-dev"
に次の行を追加します。
"phpunit/php-invoker": "*"PHPUnit_Selenium
PHPUnit 用の Selenium RC インテグレーション。
このパッケージは、PEAR を使って次のコマンドでインストールします。
pear install phpunit/PHPUnit_Selenium
composer でインストールするには、
"require-dev"
に次の行を追加します。
"phpunit/phpunit-selenium": ">=1.2"PHPUnit_Story
PHPUnit で振る舞い駆動開発をするための、ストーリーベースのテストランナー。
このパッケージは、PEAR を使って次のコマンドでインストールします。
pear install phpunit/PHPUnit_Story
composer でインストールするには、
"require-dev"
に次の行を追加します。
"phpunit/phpunit-story": "*"PHPUnit_SkeletonGenerator
プロダクションコードのクラスからテストクラスの雛形を生成したり、 その逆の操作をしたりするツール。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_SkeletonGeneratorPHPUnit_TestListener_DBUS
イベントを DBUS に送信するテストリスナー。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_TestListener_DBUSPHPUnit_TestListener_XHProf
XHProf を使ってテスト対象コードの自動プロファイリングを行うテストリスナー。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_TestListener_XHProfPHPUnit_TicketListener_Fogbugz
Fogbugz issue API 用のチケットリスナー。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_TicketListener_FogbugzPHPUnit_TicketListener_GitHub
GitHub issue API 用のチケットリスナー。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_TicketListener_GitHubPHPUnit_TicketListener_GoogleCode
Google Code issue API 用のチケットリスナー。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_TicketListener_GoogleCodePHPUnit_TicketListener_Trac
Trac issue API 用のチケットリスナー。
このパッケージは、次のコマンドでインストールします。
pear install phpunit/PHPUnit_TicketListener_TracPHPUnit 3.6 から PHPUnit 3.7 へのアップグレードの際に出くわすことになる、 過去との互換性に関するちょっとした問題をまとめます。
アップグレードそのものは極めて簡単で、特に問題も発生しません。 主要なオープンソースのフレームワークのすべてを対象にテストをして、 まったく問題がありませんでした。 しかし、プロジェクトによって事情は異なるでしょう。 これまでのリリース候補を一度も試さずにアップグレードした人は戸惑うかもしれません。 そんなときは、このドキュメントが助けとなるでしょう。
PHPUnit_Extensions_OutputTestCase クラスは削除されました。
PHPUnit 3.6 のときには非推奨の notice を発行していたものです。
出力をテストする方法については
「出力内容のテスト」 をご覧ください。
テストの中で作業ディレクトリ
(cwd) を変更すると、
これまでの PHPUnit はコードカバレッジの出力で問題が発生していました。
3.7 ではテストケースごとに作業ディレクトリを復元するようになったので、
他のテストケースで作業ディレクトリを変更していることを前提としたテストケースで
問題が発生します。もともとそんな状況は好ましくないし、
簡単に修正できるでしょう。
「テストリスナー」
で説明しているカスタムテストリスナーを使うとき、
これまでの PHPUnit ではテストリスナーを読み込めなくてもエラーになりませんでした。
そのため、テストリスナーが読み込めないことが原因で問題が発生しても、
原因を追及しにくかったのです。3.7 からは autoload でクラスを探すようになりました。
テストリスナーが見つからないときは autoload でエラーになります。
autoloader がテストリスナーを見つけられないと問題になるので、
そのリスナーを削除するなり
bootstrap.php できちんと読み込むようにするなりして解決しましょう。
これまでは、モックを使うときにはすべてのオブジェクトのパラメータをクローンしていました。 そのため、メソッドに渡されたオブジェクトの同一性を確かめたいときや そもそもクローンできないオブジェクトであった場合などに問題が発生していました。 多くの人からいただいた要望に応えて、この振る舞いは変わりました。 例 10.14 に、新しい実装がいかに便利かを示す例があります。 例 10.15 で、過去のバージョンの挙動に戻す方法を説明しています。
addUncoveredFilesFromWhitelist
が削除され、
processUncoveredFilesFromWhitelist に変わった
コードカバレッジの生成で
<whitelist addUncoveredFilesFromWhitelist="true">
を使うとき、カバーされないすべてのファイルは PHPUnit に含まれませんでした。
これらのファイルに実行可能なコードを配置している人にとっては、これは問題でした。
PHPUnit 3.7 からは、ファイルをスキャンしてどのコードが実行可能かを調べ、
どのコードを除外してはいけないかを調べるようになりました。
その結果、コードカバレッジの結果が変わることになるかもしれません。
過去のバージョンと同じ挙動にするには、
<whitelist processUncoveredFilesFromWhitelist=="true">
と設定します。PHPUnit 3.6 での挙動と 3.7 での挙動のどちらを使いたい人もいるので、
しばらくはどちらの挙動でも使えるようにしておきます。
cacheTokens のデフォルト値が
false に変わった
PHPUnit 3.7.2 からは、トークン化したファイルを
デフォルトではキャッシュしないようにしました。
大規模なプロジェクトのコードカバレッジレポートを作るときに、
キャッシュは大量のメモリを消費します。
また、ホワイトリストの仕様変更の影響もあって、
何千ものクラスを含むコードベースではキャッシュが問題になっていました。
もし比較的小規模なプロジェクトであったり十分にメモリを確保できたりする場合は、
phpunit.xml に
cacheTokens="true" を追加してください。
詳しくは 「PHPUnit」 をご覧ください。
例 4.1 で、 PHP の配列操作のテストを PHPUnit 用に書く方法を示します。 この例では、PHPUnit を使ったテストを書く際の基本的な決まり事や手順を紹介します。
Class という名前のクラスのテストは、ClassTest という名前のクラスに記述します。
ClassTest は、(ほとんどの場合) PHPUnit_Framework_TestCase を継承します。
テストは、test* という名前のパブリックメソッドとなります。
あるいは、@test アノテーションをメソッドのコメント部で使用することで、それがテストメソッドであることを示すこともできます。
テストメソッドの中で assertEquals() のようなアサーションメソッド (「アサーション」 を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。
例 4.1: PHPUnit での配列操作のテスト
<?php
class StackTest extends PHPUnit_Framework_TestCase
{
public function testPushAndPop()
{
$stack = array();
$this->assertEquals(0, count($stack));
array_push($stack, 'foo');
$this->assertEquals('foo', $stack[count($stack)-1]);
$this->assertEquals(1, count($stack));
$this->assertEquals('foo', array_pop($stack));
$this->assertEquals(0, count($stack));
}
}
?>
Whenever you are tempted to type something into a
何かを | ||
| --Martin Fowler | ||
Unit Tests are primarily written as a good practice to help developers identify and fix bugs, to refactor code and to serve as documentation for a unit of software under test. To achieve these benefits, unit tests ideally should cover all the possible paths in a program. One unit test usually covers one specific path in one function or method. However a test method is not necessary an encapsulated, independent entity. Often there are implicit dependencies between test methods, hidden in the implementation scenario of a test. ユニットテストを書くそもそもの目的は、バグの発見と修正や コードのリファクタリングを開発者がやりやすくすること。 そしてテスト対象のソフトウェアのドキュメントとしての役割を果たすことだ。 これらの目的を達成するためには、 ユニットテストがプログラム内のすべてのルートをカバーしていることが理想である。 ひとつのユニットテストがカバーするのは、 通常はひとつの関数やメソッド内の特定のルートだけとなる。 しかし、テストメソッドは必ずしもカプセル化して独立させる必要はない。 複数のテストメソッドの間に暗黙の依存性があって、 隠された実装シナリオがテストの中にあるのもよくあることだ。 | ||
| --Adrian Kuhn et. al. | ||
PHPUnit は、テストメソッド間の依存性の明示的な宣言をサポートしています。 この依存性とは、テストメソッドが実行される順序を定義するものではありません。 プロデューサーがテストフィクスチャを作ってそのインスタンスを返し、 依存するコンシューマーがそれを受け取って利用するというものです。
プロデューサーとは、返り値としてテスト対象のユニットを生成するテストメソッドのこと。
コンシューマーとは、プロデューサーの返り値に依存するテストメソッドのこと。
例 4.2
は、@depends アノテーションを使ってテストメソッドの依存性をあらわす例です。
例 4.2: @depends アノテーションを使った依存性の表現
<?php
class StackTest extends PHPUnit_Framework_TestCase
{
public function testEmpty()
{
$stack = array();
$this->assertEmpty($stack);
return $stack;
}
/**
* @depends testEmpty
*/
public function testPush(array $stack)
{
array_push($stack, 'foo');
$this->assertEquals('foo', $stack[count($stack)-1]);
$this->assertNotEmpty($stack);
return $stack;
}
/**
* @depends testPush
*/
public function testPop(array $stack)
{
$this->assertEquals('foo', array_pop($stack));
$this->assertEmpty($stack);
}
}
?>
上の例では、まず最初のテスト testEmpty()
で新しい配列を作り、それが空であることを確かめます。
このテストは、フィクスチャを返します。
二番目のテスト testPush() は
testEmpty() に依存しており、
依存するテストの結果を引数として受け取ります。
最後の testPop() は
testPush() に依存しています。
問題の局所化を手早く行うには、失敗したテストに目を向けやすくしたいものです。 そのため PHPUnit では、 あるテストが失敗したときにはそのテストに依存する他のテストの実行をスキップします。 テスト間の依存性を活用して問題点を見つけやすくしている例を 例 4.3 に示します。
例 4.3: テストの依存性の活用
<?php
class DependencyFailureTest extends PHPUnit_Framework_TestCase
{
public function testOne()
{
$this->assertTrue(FALSE);
}
/**
* @depends testOne
*/
public function testTwo()
{
}
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FS
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) DependencyFailureTest::testOne
Failed asserting that false is true.
/home/sb/DependencyFailureTest.php:6
There was 1 skipped test:
1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass.
FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.
ひとつのテストに複数の @depends アノテーションをつけることもできます。
PHPUnit はテストが実行される順序を変更しないので、
テストが実行されるときに確実に依存性が満たされているようにしておく必要があります。
テストメソッドには任意の引数を渡すことができます。
この引数は、データプロバイダメソッド
(例 4.4
の provider())
で指定します。使用するデータプロバイダメソッドを指定するには
@dataProvider アノテーションを使用します。
データプロバイダメソッドは、public
でなければなりません。また、
メソッドの返り値の型は、配列の配列あるいはオブジェクト
(Iterator インターフェイスを実装しており、
反復処理の際に配列を返すもの) である必要があります。
この返り値の各要素に対して、その配列の中身を引数としてテストメソッドがコールされます。
例 4.4: 配列の配列を返すデータプロバイダの使用
<?php
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider provider
*/
public function testAdd($a, $b, $c)
{
$this->assertEquals($c, $a + $b);
}
public function provider()
{
return array(
array(0, 0, 0),
array(0, 1, 1),
array(1, 0, 1),
array(1, 1, 3)
);
}
}
?>
phpunit DataTest
PHPUnit 3.7.0 by Sebastian Bergmann.
...F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 matches expected 3.
/home/sb/DataTest.php:9
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.例 4.5: Iterator オブジェクトを返すデータプロバイダの使用
<?php
require 'CsvFileIterator.php';
class DataTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider provider
*/
public function testAdd($a, $b, $c)
{
$this->assertEquals($c, $a + $b);
}
public function provider()
{
return new CsvFileIterator('data.csv');
}
}
?>
phpunit DataTest
PHPUnit 3.7.0 by Sebastian Bergmann.
...F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 matches expected '3'.
/home/sb/DataTest.php:11
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.例 4.6: CsvFileIterator クラス
<?php
class CsvFileIterator implements Iterator {
protected $file;
protected $key = 0;
protected $current;
public function __construct($file) {
$this->file = fopen($file, 'r');
}
public function __destruct() {
fclose($this->file);
}
public function rewind() {
rewind($this->file);
$this->current = fgetcsv($this->file);
$this->key = 0;
}
public function valid() {
return !feof($this->file);
}
public function key() {
return $this->key;
}
public function current() {
return $this->current;
}
public function next() {
$this->current = fgetcsv($this->file);
$this->key++;
}
}
?>
@dataProvider で指定したメソッドと
@depends で指定したテストの両方からの入力を受け取るテストの場合、
データプロバイダからの引数のほうが依存するテストからの引数より先にきます。
例 4.7
は、テストするコード内で例外がスローされたかどうかを
@expectedException アノテーションを使用して調べる方法を示すものです。
例 4.7: @expectedException アノテーションの使用法
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
}
}
?>
phpunit ExceptionTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ExceptionTest::testException
Expected exception InvalidArgumentException
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
さらに、@expectedExceptionMessage や
@expectedExceptionCode を
@expectedException と組み合わせて使うと、
例外メッセージや例外コードを
例 4.8
のようにテストできます。
例 4.8: @expectedExceptionMessage および @expectedExceptionCode アノテーションの使用法
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Right Message
*/
public function testExceptionHasRightMessage()
{
throw new InvalidArgumentException('Some Message', 10);
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionCode 20
*/
public function testExceptionHasRightCode()
{
throw new InvalidArgumentException('Some Message', 10);
}
}
?>
phpunit ExceptionTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FF
Time: 0 seconds, Memory: 3.00Mb
There were 2 failures:
1) ExceptionTest::testExceptionHasRightMessage
Failed asserting that exception message 'Some Message' contains 'Right Message'.
2) ExceptionTest::testExceptionHasRightCode
Failed asserting that expected exception code 20 is equal to 10.
FAILURES!
Tests: 2, Assertions: 4, Failures: 2.
@expectedExceptionMessage や @expectedExceptionCode
を使ったその他の例が、それぞれ
「@expectedExceptionMessage」 と
「@expectedExceptionCode」 にあります。
一方、setExpectedException()
メソッドを使用して、発生するであろう例外を指定することもできます。この方法を
例 4.9
に示します。
例 4.9: テスト対象のコードで発生するであろう例外の指定
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase
{
public function testException()
{
$this->setExpectedException('InvalidArgumentException');
}
public function testExceptionHasRightMessage()
{
$this->setExpectedException(
'InvalidArgumentException', 'Right Message'
);
throw new InvalidArgumentException('Some Message', 10);
}
public function testExceptionHasRightCode()
{
$this->setExpectedException(
'InvalidArgumentException', 'Right Message', 20
);
throw new InvalidArgumentException('The Right Message', 10);
}
}?>
phpunit ExceptionTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FFF
Time: 0 seconds, Memory: 3.00Mb
There were 3 failures:
1) ExceptionTest::testException
Expected exception InvalidArgumentException
2) ExceptionTest::testExceptionHasRightMessage
Failed asserting that exception message 'Some Message' contains 'Right Message'.
3) ExceptionTest::testExceptionHasRightCode
Failed asserting that expected exception code 20 is equal to 10.
FAILURES!
Tests: 3, Assertions: 6, Failures: 3.表 4.1 は、例外をテストするために用意されているメソッドをまとめたものです。
表4.1 例外のテスト用のメソッド
| メソッド | 意味 |
|---|---|
void setExpectedException(string $exceptionName[, string $exceptionMessage = '', integer $exceptionCode = NULL]) | 期待する $exceptionName、$exceptionMessage および $exceptionCode を設定します。 |
String getExpectedException() | 発生することを期待する例外の名前を返します。 |
一方、 例 4.10 のような方法で例外をテストすることもできます。
例 4.10: 例外をテストするための、別の方法
<?php
class ExceptionTest extends PHPUnit_Framework_TestCase {
public function testException() {
try {
// ... 例外が発生するであろうコード ...
}
catch (InvalidArgumentException $expected) {
return;
}
$this->fail('期待通りの例外が発生しませんでした。');
}
}
?>
例外が発生するはずの
例 4.10
のコードで例外が発生しなかった場合、それに続く
fail()
によってテストが終了し、問題を報告します。期待通りに例外が発生すると、
catch ブロックが実行されてテストは正常終了します。
デフォルトでは、PHPUnit はテストの実行中に発生した PHP のエラーや警告そして notice を例外に変換します。これらの例外を用いて、たとえば 例 4.11 のように PHP のエラーが発生することをテストできます。
例 4.11: @expectedException を用いた、PHP エラーが発生することのテスト
<?php
class ExpectedErrorTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException PHPUnit_Framework_Error
*/
public function testFailingInclude()
{
include 'not_existing_file.php';
}
}
?>
phpunit ExpectedErrorTest
PHPUnit 3.7.0 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 1 assertion)
PHPUnit_Framework_Error_Notice および
PHPUnit_Framework_Error_Warning は、
それぞれ PHP の notice と警告に対応します。
例外をテストするときには可能な限り限定的にしなければいけません。
あまりに一般化されすぎたクラスをテストすると、予期せぬ副作用を引き起こしかねません。
というわけで、
@expectedException や
setExpectedException()
を使った Exception
クラスのテストはできないようにしました。
エラーを引き起こすような PHP の関数、たとえば fopen
などに依存するテストを行うときには、テスト中にエラーを抑制できれば便利なことがあります。
そうすれば、notice のせいで
PHPUnit_Framework_Error_Notice
が出てしまうことなく、返り値だけをチェックできるようになります。
例 4.12: PHP のエラーが発生するコードの返り値のテスト
<?php
class ErrorSuppressionTest extends PHPUnit_Framework_TestCase
{
public function testFileWriting() {
$writer = new FileWriter;
$this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
}
}
class FileWriter
{
public function write($file, $content) {
$file = fopen($file, 'w');
if($file == false) {
return false;
}
// ...
}
}
?>
phpunit ErrorSuppressionTest
PHPUnit 3.7.0 by Sebastian Bergmann.
.
Time: 1 seconds, Memory: 5.25Mb
OK (1 test, 1 assertion)
もしエラーを抑制しなければ、このテストは失敗して
fopen(/is-not-writeable/file): failed to open stream:
No such file or directory となります。
メソッドの実行結果を確かめる方法として、(echo や
print などによる)
出力が期待通りのものかを調べたいこともあるでしょう。
PHPUnit_Framework_TestCase クラスは、PHP の
出力バッファリング 機能を使用してこの仕組みを提供します。
例 4.13
では、期待する出力内容を expectOutputString()
メソッドで設定する方法を示します。
期待通りの出力が得られなかった場合は、そのテストは失敗という扱いになります。
例 4.13: 関数やメソッドの出力内容のテスト
<?php
class OutputTest extends PHPUnit_Framework_TestCase
{
public function testExpectFooActualFoo()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectBarActualBaz()
{
$this->expectOutputString('bar');
print 'baz';
}
}
?>
phpunit OutputTest
PHPUnit 3.7.0 by Sebastian Bergmann.
.F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.表 4.2 は、 テストの出力用に提供するメソッドをまとめたものです。
表4.2 テストの出力用のメソッド
| メソッド | 意味 |
|---|---|
void expectOutputRegex(string $regularExpression) | 出力が正規表現 $regularExpression にマッチするであろうという予測を設定します。 |
void expectOutputString(string $expectedString) | 出力が文字列 $expectedString と等しくなるであろうという予測を設定します。 |
bool setOutputCallback(callable $callback) | たとえば出力時の正規化などに使用するコールバック関数を設定します。 |
PHPUnit は、テストの実行時に発生するすべての出力を取り込むことに注意しましょう。 strict モードでは、出力を発生させるテストは失敗します。
この節では、利用可能なアサーションメソッドの一覧を示します。
assertArrayHasKey(mixed $key, array $array[, string $message = ''])
$array にキー $key が存在しない場合にエラー $message を報告します。
assertArrayNotHasKey() はこのアサーションの逆で、同じ引数をとります。
例 4.14: assertArrayHasKey() の使用法
<?php
class ArrayHasKeyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertArrayHasKey('foo', array('bar' => 'baz'));
}
}
?>
phpunit ArrayHasKeyTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ArrayHasKeyTest::testFailure
Failed asserting that an array has the key 'foo'.
/home/sb/ArrayHasKeyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasAttribute(string $attributeName, string $className[, string $message = ''])
$className::attributeName が存在しない場合にエラー $message を報告します。
assertClassNotHasAttribute() はこのアサーションの逆で、同じ引数をとります。
例 4.15: assertClassHasAttribute() の使用法
<?php
class ClassHasAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertClassHasAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassHasAttributeTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ClassHasAttributeTest::testFailure
Failed asserting that class "stdClass" has attribute "foo".
/home/sb/ClassHasAttributeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasStaticAttribute(string $attributeName, string $className[, string $message = ''])
$className::attributeName が存在しない場合にエラー $message を報告します。
assertClassNotHasStaticAttribute() はこのアサーションの逆で、同じ引数をとります。
例 4.16: assertClassHasStaticAttribute() の使用法
<?php
class ClassHasStaticAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertClassHasStaticAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassHasStaticAttributeTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ClassHasStaticAttributeTest::testFailure
Failed asserting that class "stdClass" has static attribute "foo".
/home/sb/ClassHasStaticAttributeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])
$needle が $haystack の要素でない場合にエラー $message を報告します。
assertNotContains() はこのアサーションの逆で、同じ引数をとります。
assertAttributeContains() と assertAttributeNotContains() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を haystack として使用することができます。
例 4.17: assertContains() の使用法
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContains(4, array(1, 2, 3));
}
}
?>
phpunit ContainsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsTest::testFailure
Failed asserting that an array contains 4.
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(string $needle, string $haystack[, string $message = ''])
$needle が $haystack の部分文字列でない場合にエラー $message を報告します。
例 4.18: assertContains() の使用法
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContains('baz', 'foobar');
}
}
?>
phpunit ContainsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsTest::testFailure
Failed asserting that 'foobar' contains "baz".
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])
$haystack の中身の型が $type だけではない場合にエラー $message を報告します。
$isNativeType はフラグで、$type がネイティブな PHP の型であるかどうかを表します。
assertNotContainsOnly() はこのアサーションの逆で、同じ引数をとります。
assertAttributeContainsOnly() と assertAttributeNotContainsOnly() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を haystack として使用することができます。
例 4.19: assertContainsOnly() の使用法
<?php
class ContainsOnlyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContainsOnly('string', array('1', '2', 3));
}
}
?>
phpunit ContainsOnlyTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsOnlyTest::testFailure
Failed asserting that Array (
0 => '1'
1 => '2'
2 => 3
) contains only values of type "string".
/home/sb/ContainsOnlyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContainsOnlyInstancesOf(string $classname, Traversable|array $haystack[, string $message = ''])
$haystack が $classname クラスの唯一のインスタンスを含まない場合にエラー $message を報告します。
例 4.20: assertContainsOnlyInstancesOf() の使用法
<?php
class ContainsOnlyInstancesOfTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContainsOnlyInstancesOf('Foo', array(new Foo(), new Bar(), new Foo()));
}
}
?>
phpunit ContainsOnlyInstancesOfTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) ContainsOnlyInstancesOfTest::testFailure
Failed asserting that Array ([0]=> Bar Object(...)) is an instance of class "Foo".
/home/sb/ContainsOnlyInstancesOfTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertCount($expectedCount, $haystack[, string $message = ''])
$haystack の要素数が $expectedCount でない場合にエラー $message を報告します。
assertNotCount() はこのアサーションの逆で、同じ引数をとります。
例 4.21: assertCount() の使用法
<?php
class CountTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertCount(0, array('foo'));
}
}
?>
phpunit CountTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) CountTest::testFailure
Failed asserting that actual size 1 matches expected size 0.
/home/sb/CountTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEmpty(mixed $actual[, string $message = ''])
$actual が空でない場合にエラー $message を報告します。
assertNotEmpty() はこのアサーションの逆で、同じ引数をとります。
assertAttributeEmpty() および assertAttributeNotEmpty() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性に対して使えます。
例 4.22: assertEmpty() の使用法
<?php
class EmptyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertEmpty(array('foo'));
}
}
?>
phpunit EmptyTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) EmptyTest::testFailure
Failed asserting that an array is empty.
/home/sb/EmptyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement[, boolean $checkAttributes = FALSE, string $message = ''])
$actualElement の DOMElement の XML 構造が $expectedElement の DOMElement の XML 構造と等しくない場合にエラー $message を報告します。
例 4.23: assertEqualXMLStructure() の使用法
<?php
class EqualXMLStructureTest extends PHPUnit_Framework_TestCase
{
public function testFailureWithDifferentNodeNames()
{
$expected = new DOMElement('foo');
$actual = new DOMElement('bar');
$this->assertEqualXMLStructure($expected, $actual);
}
public function testFailureWithDifferentNodeAttributes()
{
$expected = new DOMDocument;
$expected->loadXML('<foo bar="true" />');
$actual = new DOMDocument;
$actual->loadXML('<foo/>');
$this->assertEqualXMLStructure(
$expected->firstChild, $actual->firstChild, TRUE
);
}
public function testFailureWithDifferentChildrenCount()
{
$expected = new DOMDocument;
$expected->loadXML('<foo><bar/><bar/><bar/></foo>');
$actual = new DOMDocument;
$actual->loadXML('<foo><bar/></foo>');
$this->assertEqualXMLStructure(
$expected->firstChild, $actual->firstChild
);
}
public function testFailureWithDifferentChildren()
{
$expected = new DOMDocument;
$expected->loadXML('<foo><bar/><bar/><bar/></foo>');
$actual = new DOMDocument;
$actual->loadXML('<foo><baz/><baz/><baz/></foo>');
$this->assertEqualXMLStructure(
$expected->firstChild, $actual->firstChild
);
}
}
?>
phpunit EqualXMLStructureTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.75Mb
There were 4 failures:
1) EqualXMLStructureTest::testFailureWithDifferentNodeNames
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'foo'
+'bar'
/home/sb/EqualXMLStructureTest.php:9
2) EqualXMLStructureTest::testFailureWithDifferentNodeAttributes
Number of attributes on node "foo" does not match
Failed asserting that 0 matches expected 1.
/home/sb/EqualXMLStructureTest.php:22
3) EqualXMLStructureTest::testFailureWithDifferentChildrenCount
Number of child nodes of "foo" differs
Failed asserting that 1 matches expected 3.
/home/sb/EqualXMLStructureTest.php:35
4) EqualXMLStructureTest::testFailureWithDifferentChildren
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
/home/sb/EqualXMLStructureTest.php:48
FAILURES!
Tests: 4, Assertions: 8, Failures: 4.assertEquals(mixed $expected, mixed $actual[, string $message = ''])
2 つの変数 $expected と $actual が等しくない場合にエラー $message を報告します。
assertNotEquals() はこのアサーションの逆で、同じ引数をとります。
assertAttributeEquals() と assertAttributeNotEquals() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を実際の値として使用することができます。
例 4.24: assertEquals() の使用法
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertEquals(1, 0);
}
public function testFailure2()
{
$this->assertEquals('bar', 'baz');
}
public function testFailure3()
{
$this->assertEquals("foo\nbar\nbaz\n", "foo\nbah\nbaz\n");
}
}
?>
phpunit EqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FFF
Time: 0 seconds, Memory: 5.25Mb
There were 3 failures:
1) EqualsTest::testFailure
Failed asserting that 0 matches expected 1.
/home/sb/EqualsTest.php:6
2) EqualsTest::testFailure2
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'
/home/sb/EqualsTest.php:11
3) EqualsTest::testFailure3
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
'foo
-bar
+bah
baz
'
/home/sb/EqualsTest.php:16
FAILURES!
Tests: 3, Assertions: 3, Failures: 3.引数 $expected と $actual の型により特化した比較については、以下を参照ください。
assertEquals(float $expected, float $actual[, string $message = '', float $delta = 0])
2 つの float 値 $expected と $actual の誤差が $delta より大きい場合にエラー $message を報告します。
なぜ $delta が必要となるのかについては "What Every Computer Scientist Should Know About Floating-Point Arithmetic" を参照ください。
例 4.25: float 値での assertEquals() の使用法
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testSuccess()
{
$this->assertEquals(1.0, 1.1, '', 0.2);
}
public function testFailure()
{
$this->assertEquals(1.0, 1.1);
}
}
?>
phpunit EqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
.F
Time: 0 seconds, Memory: 5.75Mb
There was 1 failure:
1) EqualsTest::testFailure
Failed asserting that 1.1 matches expected 1.0.
/home/sb/EqualsTest.php:11
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.assertEquals(DOMDocument $expected, DOMDocument $actual[, string $message = ''])
2 つの DOMDocument オブジェクト $expected と $actual で表される XML ドキュメントが (コメントを除去して正規化した状態で) 等しくない場合にエラー $message を報告します。
例 4.26: DOMDocument オブジェクトでの assertEquals() の使用法
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$expected = new DOMDocument;
$expected->loadXML('<foo><bar/></foo>');
$actual = new DOMDocument;
$actual->loadXML('<bar><foo/></bar>');
$this->assertEquals($expected, $actual);
}
}
?>
phpunit EqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) EqualsTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
-<foo>
- <bar/>
-</foo>
+<bar>
+ <foo/>
+</bar>
/home/sb/EqualsTest.php:12
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEquals(object $expected, object $actual[, string $message = ''])
2 つのオブジェクト $expected と $actual が同じ属性値を持たない場合にエラー $message を報告します。
例 4.27: オブジェクトでの assertEquals() の使用法
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$expected = new stdClass;
$expected->foo = 'foo';
$expected->bar = 'bar';
$actual = new stdClass;
$actual->foo = 'bar';
$actual->baz = 'bar';
$this->assertEquals($expected, $actual);
}
}
?>
phpunit EqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) EqualsTest::testFailure
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
stdClass Object (
- 'foo' => 'foo'
- 'bar' => 'bar'
+ 'foo' => 'bar'
+ 'baz' => 'bar'
)
/home/sb/EqualsTest.php:14
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertEquals(array $expected, array $actual[, string $message = ''])
2 つの配列 $expected と $actual が等しくない場合にエラー $message を報告します。
例 4.28: 配列での assertEquals() の使用法
<?php
class EqualsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertEquals(array('a', 'b', 'c'), array('a', 'c', 'd'));
}
}
?>
phpunit EqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) EqualsTest::testFailure
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
0 => 'a'
- 1 => 'b'
- 2 => 'c'
+ 1 => 'c'
+ 2 => 'd'
)
/home/sb/EqualsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertFalse(bool $condition[, string $message = ''])
$condition が TRUE の場合にエラー $message を報告します。
例 4.29: assertFalse() の使用法
<?php
class FalseTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertFalse(TRUE);
}
}
?>
phpunit FalseTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) FalseTest::testFailure
Failed asserting that true is false.
/home/sb/FalseTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertFileEquals(string $expected, string $actual[, string $message = ''])
$expected で指定したファイルと $actual で指定したファイルの内容が異なる場合にエラー $message を報告します。
assertFileNotEquals() はこのアサーションの逆で、同じ引数をとります。
例 4.30: assertFileEquals() の使用法
<?php
class FileEqualsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertFileEquals('/home/sb/expected', '/home/sb/actual');
}
}
?>
phpunit FileEqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) FileEqualsTest::testFailure
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'expected
+'actual
'
/home/sb/FileEqualsTest.php:6
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertFileExists(string $filename[, string $message = ''])
ファイル $filename が存在しない場合にエラー $message を報告します。
assertFileNotExists() はこのアサーションの逆で、同じ引数をとります。
例 4.31: assertFileExists() の使用法
<?php
class FileExistsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertFileExists('/path/to/file');
}
}
?>
phpunit FileExistsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) FileExistsTest::testFailure
Failed asserting that file "/path/to/file" exists.
/home/sb/FileExistsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertGreaterThan(mixed $expected, mixed $actual[, string $message = ''])
$actual の値が $expected の値より大きくない場合にエラー $message を報告します。
assertAttributeGreaterThan() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を実際の値として使用することができます。
例 4.32: assertGreaterThan() の使用法
<?php
class GreaterThanTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertGreaterThan(2, 1);
}
}
?>
phpunit GreaterThanTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) GreaterThanTest::testFailure
Failed asserting that 1 is greater than 2.
/home/sb/GreaterThanTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertGreaterThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])
$actual の値が $expected の値以上でない場合にエラー $message を報告します。
assertAttributeGreaterThanOrEqual() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を実際の値として使用することができます。
例 4.33: assertGreaterThanOrEqual() の使用法
<?php
class GreatThanOrEqualTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertGreaterThanOrEqual(2, 1);
}
}
?>
phpunit GreaterThanOrEqualTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) GreatThanOrEqualTest::testFailure
Failed asserting that 1 is equal to 2 or is greater than 2.
/home/sb/GreaterThanOrEqualTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertInstanceOf($expected, $actual[, $message = ''])
$actual が $expected のインスタンスでない場合にエラー $message を報告します。
assertNotInstanceOf() はこのアサーションの逆で、同じ引数をとります。
assertAttributeInstanceOf() および assertAttributeNotInstanceOf() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性に対して使えます。
例 4.34: assertInstanceOf() の使用法
<?php
class InstanceOfTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertInstanceOf('RuntimeException', new Exception);
}
}
?>
phpunit InstanceOfTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) InstanceOfTest::testFailure
Failed asserting that Exception Object (...) is an instance of class "RuntimeException".
/home/sb/InstanceOfTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertInternalType($expected, $actual[, $message = ''])
$actual の型が $expected でない場合にエラー $message を報告します。
assertNotInternalType() はこのアサーションの逆で、同じ引数をとります。
assertAttributeInternalType() および assertAttributeNotInternalType() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性に対して使えます。
例 4.35: assertInternalType() の使用法
<?php
class InternalTypeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertInternalType('string', 42);
}
}
?>
phpunit InternalTypeTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) InternalTypeTest::testFailure
Failed asserting that 42 is of type "string".
/home/sb/InternalTypeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertJsonFileEqualsJsonFile(mixed $expectedFile, mixed $actualFile[, string $message = ''])
$actualFile の値が
$expectedFile の値にマッチしない場合にエラー $message を報告します。
例 4.36: assertJsonFileEqualsJsonFile() の使用法
<?php
class JsonFileEqualsJsonFileTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertJsonFileEqualsJsonFile(
'path/to/fixture/file', 'path/to/actual/file');
}
}
?>
phpunit JsonFileEqualsJsonFileTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) JsonFileEqualsJsonFile::testFailure
Failed asserting that '{"Mascott":"Tux"}' matches JSON string "["Mascott", "Tux", "OS", "Linux"]".
/home/sb/JsonFileEqualsJsonFileTest.php:5
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertJsonStringEqualsJsonFile(mixed $expectedFile, mixed $actualJson[, string $message = ''])
$actualJson の値が
$expectedFile の値にマッチしない場合にエラー $message を報告します。
例 4.37: assertJsonStringEqualsJsonFile() の使用法
<?php
class JsonStringEqualsJsonFileTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertJsonStringEqualsJsonFile(
'path/to/fixture/file', json_encode(array("Mascott" => "ux"));
}
}
?>
phpunit JsonStringEqualsJsonFileTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) JsonStringEqualsJsonFile::testFailure
Failed asserting that '{"Mascott":"ux"}' matches JSON string "{"Mascott":"Tux"}".
/home/sb/JsonStringEqualsJsonFileTest.php:5
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertJsonStringEqualsJsonString(mixed $expectedJson, mixed $actualJson[, string $message = ''])
$actualJson の値が
$expectedJson の値にマッチしない場合にエラー $message を報告します。
例 4.38: assertJsonStringEqualsJsonString() の使用法
<?php
class JsonStringEqualsJsonStringTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertJsonStringEqualsJsonString(
json_encode(array("Mascott" => "Tux"), json_encode(array("Mascott" => "ux"));
}
}
?>
phpunit JsonStringEqualsJsonStringTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) JsonStringEqualsJsonStringTest::testFailure
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
stdClass Object (
- 'Mascott' => 'Tux'
+ 'Mascott' => 'ux'
)
/home/sb/JsonStringEqualsJsonStringTest.php:5
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertLessThan(mixed $expected, mixed $actual[, string $message = ''])
$actual の値が $expected の値より小さくない場合にエラー $message を報告します。
assertAttributeLessThan() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を実際の値として使用することができます。
例 4.39: assertLessThan() の使用法
<?php
class LessThanTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertLessThan(1, 2);
}
}
?>
phpunit LessThanTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) LessThanTest::testFailure
Failed asserting that 2 is less than 1.
/home/sb/LessThanTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertLessThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])
$actual の値が $expected の値以下でない場合にエラー $message を報告します。
assertAttributeLessThanOrEqual() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を実際の値として使用することができます。
例 4.40: assertLessThanOrEqual() の使用法
<?php
class LessThanOrEqualTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertLessThanOrEqual(1, 2);
}
}
?>
phpunit LessThanOrEqualTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) LessThanOrEqualTest::testFailure
Failed asserting that 2 is equal to 1 or is less than 1.
/home/sb/LessThanOrEqualTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertNull(mixed $variable[, string $message = ''])
$variable が NULL でないときにエラー $message を報告します。
assertNotNull() はこのアサーションの逆で、同じ引数をとります。
例 4.41: assertNull() の使用法
<?php
class NullTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertNull('foo');
}
}
?>
phpunit NotNullTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) NullTest::testFailure
Failed asserting that 'foo' is null.
/home/sb/NotNullTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertObjectHasAttribute(string $attributeName, object $object[, string $message = ''])
$object->attributeName が存在しない場合にエラー $message を報告します。
assertObjectNotHasAttribute() はこのアサーションの逆で、同じ引数をとります。
例 4.42: assertObjectHasAttribute() の使用法
<?php
class ObjectHasAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertObjectHasAttribute('foo', new stdClass);
}
}
?>
phpunit ObjectHasAttributeTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) ObjectHasAttributeTest::testFailure
Failed asserting that object of class "stdClass" has attribute "foo".
/home/sb/ObjectHasAttributeTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertRegExp(string $pattern, string $string[, string $message = ''])
$string が正規表現 $pattern にマッチしない場合にエラー $message を報告します。
assertNotRegExp() はこのアサーションの逆で、同じ引数をとります。
例 4.43: assertRegExp() の使用法
<?php
class RegExpTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertRegExp('/foo/', 'bar');
}
}
?>
phpunit RegExpTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) RegExpTest::testFailure
Failed asserting that 'bar' matches PCRE pattern "/foo/".
/home/sb/RegExpTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertStringMatchesFormat(string $format, string $string[, string $message = ''])
$string が書式文字列 $format にマッチしない場合にエラー $message を報告します。
assertStringNotMatchesFormat() はこのアサーションの逆で、同じ引数をとります。
例 4.44: assertStringMatchesFormat() の使用法
<?php
class StringMatchesFormatTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertStringMatchesFormat('%i', 'foo');
}
}
?>
phpunit StringMatchesFormatTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) StringMatchesFormatTest::testFailure
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+$/s".
/home/sb/StringMatchesFormatTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.書式文字列には次のプレースホルダを含めることができます。
%e: ディレクトリ区切り文字、たとえば Linux なら / を表します。
%s: 一文字以上の何か (文字あるいは空白)、ただし改行文字は含みません。
%S: ゼロ文字以上の何か (文字あるいは空白)、ただし改行文字は含みません。
%a: 一文字以上の何か (文字あるいは空白)、改行文字も含みます。
%A: ゼロ文字以上の何か (文字あるいは空白)、改行文字も含みます。
%w: ゼロ文字以上の空白。
%i: 符号付き整数値。例: +3142, -3142
%d: 符号なし整数値。例: 123.66
%x: 一文字以上の十六進文字 (0-9, a-f, A-F)。
%f: 浮動小数点数値。例: 3.142, -3.142, 3.142E-10, 3.142e+10
%c: 任意の一文字。
assertStringMatchesFormatFile(string $formatFile, string $string[, string $message = ''])
$string が $formatFile の内容にマッチしない場合にエラー $message を報告します。
assertStringNotMatchesFormatFile() はこのアサーションの逆で、同じ引数をとります。
例 4.45: assertStringMatchesFormatFile() の使用法
<?php
class StringMatchesFormatFileTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertStringMatchesFormatFile('/path/to/expected.txt', 'foo');
}
}
?>
phpunit StringMatchesFormatFileTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) StringMatchesFormatFileTest::testFailure
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+
$/s".
/home/sb/StringMatchesFormatFileTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertSame(mixed $expected, mixed $actual[, string $message = ''])
2 つの変数 $expected と $actual が同じ型・同じ値でない場合にエラー $message を報告します。
assertNotSame() はこのアサーションの逆で、同じ引数をとります。
assertAttributeSame() と assertAttributeNotSame() は便利なラッパーで、クラスやオブジェクトの public、protected、private 属性を実際の値として使用することができます。
例 4.46: assertSame() の使用法
<?php
class SameTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertSame('2204', 2204);
}
}
?>
phpunit SameTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) SameTest::testFailure
Failed asserting that 2204 is identical to '2204'.
/home/sb/SameTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertSame(object $expected, object $actual[, string $message = ''])
2 つの変数 $expected と $actual が同じオブジェクトを参照していない場合にエラー $message を報告します。
例 4.47: オブジェクトでの assertSame() の使用法
<?php
class SameTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertSame(new stdClass, new stdClass);
}
}
?>
phpunit SameTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.75Mb
There was 1 failure:
1) SameTest::testFailure
Failed asserting that two variables reference the same object.
/home/sb/SameTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertSelectCount(array $selector, integer $count, mixed $actual[, string $message = '', boolean $isHtml = TRUE])
CSS セレクタ $selector が DOMNode $actual の $count 要素にマッチしない場合にエラー $message を報告します。
$count には次の型のいずれかを指定できます。
boolean: セレクタにマッチする要素が存在する (TRUE) か存在しない (FALSE) かを調べます。integer: 要素の数を調べます。array: 要素の数が指定した範囲にあるかどうかを調べます。<, >, <=, および >= をキーとして範囲を指定します。例 4.48: assertSelectCount() の使用法
<?php
class SelectCountTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->xml = new DomDocument;
$this->xml->loadXML('<foo><bar/><bar/><bar/></foo>');
}
public function testAbsenceFailure()
{
$this->assertSelectCount('foo bar', FALSE, $this->xml);
}
public function testPresenceFailure()
{
$this->assertSelectCount('foo baz', TRUE, $this->xml);
}
public function testExactCountFailure()
{
$this->assertSelectCount('foo bar', 5, $this->xml);
}
public function testRangeFailure()
{
$this->assertSelectCount('foo bar', array('>'=>6, '<'=>8), $this->xml);
}
}
?>
phpunit SelectCountTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.50Mb
There were 4 failures:
1) SelectCountTest::testAbsenceFailure
Failed asserting that true is false.
/home/sb/SelectCountTest.php:12
2) SelectCountTest::testPresenceFailure
Failed asserting that false is true.
/home/sb/SelectCountTest.php:17
3) SelectCountTest::testExactCountFailure
Failed asserting that 3 matches expected 5.
/home/sb/SelectCountTest.php:22
4) SelectCountTest::testRangeFailure
Failed asserting that false is true.
/home/sb/SelectCountTest.php:27
FAILURES!
Tests: 4, Assertions: 4, Failures: 4.assertSelectEquals(array $selector, string $content, integer $count, mixed $actual[, string $message = '', boolean $isHtml = TRUE])
CSS セレクタ $selector が DOMNode $actual の値 $content の $count 要素にマッチしない場合にエラー $message を報告します。
$count には次の型のいずれかを指定できます。
boolean: セレクタにマッチする要素が存在する (TRUE) か存在しない (FALSE) かを調べます。integer: 要素の数を調べます。array: 要素の数が指定した範囲にあるかどうかを調べます。<, >, <=, および >= をキーとして範囲を指定します。例 4.49: assertSelectEquals() の使用法
<?php
class SelectEqualsTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->xml = new DomDocument;
$this->xml->loadXML('<foo><bar>Baz</bar><bar>Baz</bar></foo>');
}
public function testAbsenceFailure()
{
$this->assertSelectEquals('foo bar', 'Baz', FALSE, $this->xml);
}
public function testPresenceFailure()
{
$this->assertSelectEquals('foo bar', 'Bat', TRUE, $this->xml);
}
public function testExactCountFailure()
{
$this->assertSelectEquals('foo bar', 'Baz', 5, $this->xml);
}
public function testRangeFailure()
{
$this->assertSelectEquals('foo bar', 'Baz', array('>'=>6, '<'=>8), $this->xml);
}
}
?>
phpunit SelectEqualsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.50Mb
There were 4 failures:
1) SelectEqualsTest::testAbsenceFailure
Failed asserting that true is false.
/home/sb/SelectEqualsTest.php:12
2) SelectEqualsTest::testPresenceFailure
Failed asserting that false is true.
/home/sb/SelectEqualsTest.php:17
3) SelectEqualsTest::testExactCountFailure
Failed asserting that 2 matches expected 5.
/home/sb/SelectEqualsTest.php:22
4) SelectEqualsTest::testRangeFailure
Failed asserting that false is true.
/home/sb/SelectEqualsTest.php:27
FAILURES!
Tests: 4, Assertions: 4, Failures: 4.assertSelectRegExp(array $selector, string $pattern, integer $count, mixed $actual[, string $message = '', boolean $isHtml = TRUE])
CSS セレクタ $selector が DOMNode $actual のパターン $pattern にマッチする値の $count 要素にマッチしない場合にエラー $message を報告します。
$count には次の型のいずれかを指定できます。
boolean: セレクタにマッチする要素が存在する (TRUE) か存在しない (FALSE) かを調べます。integer: 要素の数を調べます。array: 要素の数が指定した範囲にあるかどうかを調べます。<, >, <=, および >= をキーとして範囲を指定します。例 4.50: assertSelectRegExp() の使用法
<?php
class SelectRegExpTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->xml = new DomDocument;
$this->xml->loadXML('<foo><bar>Baz</bar><bar>Baz</bar></foo>');
}
public function testAbsenceFailure()
{
$this->assertSelectRegExp('foo bar', '/Ba.*/', FALSE, $this->xml);
}
public function testPresenceFailure()
{
$this->assertSelectRegExp('foo bar', '/B[oe]z]/', TRUE, $this->xml);
}
public function testExactCountFailure()
{
$this->assertSelectRegExp('foo bar', '/Ba.*/', 5, $this->xml);
}
public function testRangeFailure()
{
$this->assertSelectRegExp('foo bar', '/Ba.*/', array('>'=>6, '<'=>8), $this->xml);
}
}
?>
phpunit SelectRegExpTest
PHPUnit 3.7.0 by Sebastian Bergmann.
FFFF
Time: 0 seconds, Memory: 5.50Mb
There were 4 failures:
1) SelectRegExpTest::testAbsenceFailure
Failed asserting that true is false.
/home/sb/SelectRegExpTest.php:12
2) SelectRegExpTest::testPresenceFailure
Failed asserting that false is true.
/home/sb/SelectRegExpTest.php:17
3) SelectRegExpTest::testExactCountFailure
Failed asserting that 2 matches expected 5.
/home/sb/SelectRegExpTest.php:22
4) SelectRegExpTest::testRangeFailure
Failed asserting that false is true.
/home/sb/SelectRegExpTest.php:27
FAILURES!
Tests: 4, Assertions: 4, Failures: 4.assertStringEndsWith(string $suffix, string $string[, string $message = ''])
$string が $suffix で終わっていない場合にエラー $message を報告します。
assertStringEndsNotWith() はこのアサーションの逆で、同じ引数をとります。
例 4.51: assertStringEndsWith() の使用法
<?php
class StringEndsWithTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertStringEndsWith('suffix', 'foo');
}
}
?>
phpunit StringEndsWithTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 1 second, Memory: 5.00Mb
There was 1 failure:
1) StringEndsWithTest::testFailure
Failed asserting that 'foo' ends with "suffix".
/home/sb/StringEndsWithTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertStringEqualsFile(string $expectedFile, string $actualString[, string $message = ''])
$expectedFile で指定したファイルの内容に $actualString が含まれない場合にエラー $message を報告します。
assertStringNotEqualsFile() はこのアサーションの逆で、同じ引数をとります。
例 4.52: assertStringEqualsFile() の使用法
<?php
class StringEqualsFileTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertStringEqualsFile('/home/sb/expected', 'actual');
}
}
?>
phpunit StringEqualsFileTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) StringEqualsFileTest::testFailure
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'expected
-'
+'actual'
/home/sb/StringEqualsFileTest.php:6
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertStringStartsWith(string $prefix, string $string[, string $message = ''])
$string が $prefix で始まっていない場合にエラー $message を報告します。
assertStringStartsNotWith() はこのアサーションの逆で、同じ引数をとります。
例 4.53: assertStringStartsWith() の使用法
<?php
class StringStartsWithTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertStringStartsWith('prefix', 'foo');
}
}
?>
phpunit StringStartsWithTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) StringStartsWithTest::testFailure
Failed asserting that 'foo' starts with "prefix".
/home/sb/StringStartsWithTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertTag(array $matcher, string $actual[, string $message = '', boolean $isHtml = TRUE])
$actual が $matcher にマッチしない場合にエラー $message を報告します。
$matcher は連想配列で、アサーションに使用するマッチ条件を指定します。
id: 指定した id 属性のノードが対応する値にマッチすること。tag: ノードの型が対応する値にマッチすること。attributes: ノードの属性が、対応する値の連想配列 $attributes にマッチすること。content: テキストの内容が指定した値にマッチすること。parent: ノードの親が連想配列 $parent にマッチすること。child: ノードの直接の子のうち少なくともひとつが連想配列 $child の条件を満たすこと。ancestor: ノードの先祖のうちの少なくともひとつが連想配列 $ancestor の条件を満たすこと。descendant: ノードの子孫のうちの少なくともひとつが連想配列 $descendant の条件を満たすこと。children: ノードの子の数を数えるための連想配列。
count: マッチする子の数がこの数に等しいこと。less_than: マッチする子の数がこの数より少ないこと。greater_than: マッチする子の数がこの数より多いこと。only: 連想配列で子のマッチに使用するキーを指定し、それにマッチした子のみを数える。assertNotTag() はこのアサーションの逆で、同じ引数をとります。
例 4.54: assertTag() の使用法
<?php
// id="my_id" という要素があることを表明する matcher
$matcher = array('id' => 'my_id');
// "span" タグが存在することを表明する matcher
$matcher = array('tag' => 'span');
// 中身が "Hello World" である "span" タグが存在することを表明する
// matcher
$matcher = array('tag' => 'span', 'content' => 'Hello World');
// 正規表現で指定した内容にマッチする中身を持つ "span" タグが
// 存在することを表明する matcher
$matcher = array('tag' => 'span', 'content' => 'regexp:/Try P(HP|ython)/');
// class 属性に "list" が指定された "span" タグが存在することを表明する matcher
$matcher = array(
'tag' => 'span',
'attributes' => array('class' => 'list')
);
// "span" が "div" の内部に存在することを表明する matcher
$matcher = array(
'tag' => 'span',
'parent' => array('tag' => 'div')
);
// "span" が "table" 内のどこかに存在することを表明する matcher
$matcher = array(
'tag' => 'span',
'ancestor' => array('tag' => 'table')
);
// 子要素に少なくともひとつの "em" を持つ "span" が存在することを表明する matcher
$matcher = array(
'tag' => 'span',
'child' => array('tag' => 'em')
);
// "span" の中 (何段階か下でも可) に
// "strong" タグが存在することを表明する matcher
$matcher = array(
'tag' => 'span',
'descendant' => array('tag' => 'strong')
);
// 直接の子として 5 から 10 の "em" タグを持つ "span"
// が存在することを表明する matcher
$matcher = array(
'tag' => 'span',
'children' => array(
'less_than' => 11,
'greater_than' => 4,
'only' => array('tag' => 'em')
)
);
// "div" というタグが存在し、先祖に "ul" そして直接の親に "li"
// (class="enum") を持つこと、そして id="my_test" で中身が
// "Hello World" である "span" を子孫にもつことを表明する matcher
$matcher = array(
'tag' => 'div',
'ancestor' => array('tag' => 'ul'),
'parent' => array(
'tag' => 'li',
'attributes' => array('class' => 'enum')
),
'descendant' => array(
'tag' => 'span',
'child' => array(
'id' => 'my_test',
'content' => 'Hello World'
)
)
);
// assertTag() を使用して、$matcher を $html に適用します
$this->assertTag($matcher, $html);
// assertTag() を使用して、$matcher を $xml に適用します
$this->assertTag($matcher, $xml, '', FALSE);
?>
もっと複雑なアサーションを行う場合には、
PHPUnit_Framework_Constraint クラスを使用します。
これらは、assertThat() メソッドを使用して評価されます。
例 4.55 は、
logicalNot() と equalTo()
を用いて assertNotEquals()
と同じアサーションを行う方法を示すものです。
assertThat(mixed $value, PHPUnit_Framework_Constraint $constraint[, $message = ''])
$value が $constraint にマッチしない場合にエラー $message を報告します。
例 4.55: assertThat() の使用法
<?php
class BiscuitTest extends PHPUnit_Framework_TestCase
{
public function testEquals()
{
$theBiscuit = new Biscuit('Ginger');
$myBiscuit = new Biscuit('Ginger');
$this->assertThat(
$theBiscuit,
$this->logicalNot(
$this->equalTo($myBiscuit)
)
);
}
}
?>
表 4.3 に、
使用できる PHPUnit_Framework_Constraint クラスをまとめます。
表4.3 制約
| 制約 | 意味 |
|---|---|
PHPUnit_Framework_Constraint_Attribute attribute(PHPUnit_Framework_Constraint $constraint, $attributeName) | 別の制約を、クラスあるいはオブジェクトの属性として適用する制約。 |
PHPUnit_Framework_Constraint_IsAnything anything() | あらゆる入力値を受け入れる制約。 |
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $key) | 配列が指定したキーを保持していることを保証する制約。 |
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $value) | Iterator インターフェイスを実装している array やオブジェクトが、指定した値を保持していることを保証する制約。 |
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnly(string $type) | 評価対象の array、あるいは Iterator インターフェイスを実装したオブジェクトが、指定した型の唯一の値を含むことを保証する制約。 |
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnlyInstancesOf(string $classname) | 評価対象の array、あるいは Iterator インターフェイスを実装したオブジェクトが、指定したクラスの唯一のインスタンスを含むことを保証する制約。 |
PHPUnit_Framework_Constraint_IsEqual equalTo($value, $delta = 0, $maxDepth = 10) | ある値が別の値と等しいかどうかを調べる制約。 |
PHPUnit_Framework_Constraint_Attribute attributeEqualTo($attributeName, $value, $delta = 0, $maxDepth = 10) | ある値がクラスあるいはオブジェクトの属性と等しいかどうかを調べる制約。 |
PHPUnit_Framework_Constraint_FileExists fileExists() | 指定した名前のファイルが存在するかどうかを調べる制約。 |
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $value) | 評価される値が、指定した値より大きいことを保証する制約。 |
PHPUnit_Framework_Constraint_Or greaterThanOrEqual(mixed $value) | 評価される値が、指定した値以上であることを保証する制約。 |
PHPUnit_Framework_Constraint_ClassHasAttribute classHasAttribute(string $attributeName) | 評価されるクラスに、指定した属性があることを保証する制約。 |
PHPUnit_Framework_Constraint_ClassHasStaticAttribute classHasStaticAttribute(string $attributeName) | 評価されるクラスに、指定した static 属性があることを保証する制約。 |
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $attributeName) | 評価されるオブジェクトが、指定した属性を保持していることを保証する制約。 |
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $value) | ある値が別の値と同一であることを保証する制約。 |
PHPUnit_Framework_Constraint_IsFalse isFalse() | 評価される値が FALSE であることを保証する制約。 |
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $className) | 評価されるオブジェクトが、指定したクラスのインスタンスであることを保証する制約。 |
PHPUnit_Framework_Constraint_IsNull isNull() | 評価される値が NULL であることを保証する制約。 |
PHPUnit_Framework_Constraint_IsTrue isTrue() | 評価される値が TRUE であることを保証する制約。 |
PHPUnit_Framework_Constraint_IsType isType(string $type) | 評価される値が、指定した型であることを保証する制約。 |
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $value) | 評価される値が、指定した値より小さいことを保証する制約。 |
PHPUnit_Framework_Constraint_Or lessThanOrEqual(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) | 評価される文字列が、指定した文字列を含むことを保証する制約。 |
PHPUnit_Framework_Constraint_StringEndsWith stringEndsWith(string $suffix) | 評価される文字列が、指定したサフィックスで終わることを保証する制約。 |
PHPUnit_Framework_Constraint_StringStartsWith stringStartsWith(string $prefix) | 評価される文字列が、指定したプレフィックスで始まることを保証する制約。 |
assertTrue(bool $condition[, string $message = ''])
$condition が FALSE の場合にエラー $message を報告します。
例 4.56: assertTrue() の使用法
<?php
class TrueTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertTrue(FALSE);
}
}
?>
phpunit TrueTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) TrueTest::testFailure
Failed asserting that false is true.
/home/sb/TrueTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile[, string $message = ''])
$actualFile の XML ドキュメントが $expectedFile の XML ドキュメントと異なる場合にエラー $message を報告します。
assertXmlFileNotEqualsXmlFile() はこのアサーションの逆で、同じ引数をとります。
例 4.57: assertXmlFileEqualsXmlFile() の使用法
<?php
class XmlFileEqualsXmlFileTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertXmlFileEqualsXmlFile(
'/home/sb/expected.xml', '/home/sb/actual.xml');
}
}
?>
phpunit XmlFileEqualsXmlFileTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) XmlFileEqualsXmlFileTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
<foo>
- <bar/>
+ <baz/>
</foo>
/home/sb/XmlFileEqualsXmlFileTest.php:7
FAILURES!
Tests: 1, Assertions: 3, Failures: 1.assertXmlStringEqualsXmlFile(string $expectedFile, string $actualXml[, string $message = ''])
$actualXml の XML ドキュメントが $expectedFile の XML ドキュメントと異なる場合にエラー $message を報告します。
assertXmlStringNotEqualsXmlFile() はこのアサーションの逆で、同じ引数をとります。
例 4.58: assertXmlStringEqualsXmlFile() の使用法
<?php
class XmlStringEqualsXmlFileTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertXmlStringEqualsXmlFile(
'/home/sb/expected.xml', '<foo><baz/></foo>');
}
}
?>
phpunit XmlStringEqualsXmlFileTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) XmlStringEqualsXmlFileTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
<foo>
- <bar/>
+ <baz/>
</foo>
/home/sb/XmlStringEqualsXmlFileTest.php:7
FAILURES!
Tests: 1, Assertions: 2, Failures: 1.assertXmlStringEqualsXmlString(string $expectedXml, string $actualXml[, string $message = ''])
$actualXml の XML ドキュメントが $expectedXml の XML ドキュメントと異なる場合にエラー $message を報告します。
assertXmlStringNotEqualsXmlString() はこのアサーションの逆で、同じ引数をとります。
例 4.59: assertXmlStringEqualsXmlString() の使用法
<?php
class XmlStringEqualsXmlStringTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertXmlStringEqualsXmlString(
'<foo><bar/></foo>', '<foo><baz/></foo>');
}
}
?>
phpunit XmlStringEqualsXmlStringTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 5.00Mb
There was 1 failure:
1) XmlStringEqualsXmlStringTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
<?xml version="1.0"?>
<foo>
- <bar/>
+ <baz/>
</foo>
/home/sb/XmlStringEqualsXmlStringTest.php:7
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
phpunit コマンドを実行すると、PHPUnit
のコマンドライン版テストランナーが起動します。
コマンドラインのテストランナーを使用したテストの様子を以下に示します。
phpunit ArrayTest
PHPUnit 3.7.0 by Sebastian Bergmann.
..
Time: 0 seconds
OK (2 tests, 2 assertions)テストがひとつ実行されるたびに、PHPUnit コマンドラインツールはその経過を示す文字を出力します。
PHPUnit は、失敗 (failures) と
エラー (errors) を区別します。
「失敗」は PHPUnit のアサーションに違反した場合、つまり例えば
assertEquals() のコールに失敗した場合などで、
「エラー」は予期せぬ例外や PHP のエラーが発生した場合となります。
この区別は、時に有用です。というのは「エラー」は一般的に「失敗」
より修正しやすい傾向があるからです。
もし大量の問題が発生した場合は、まず「エラー」を最初に片付け、
その後で「失敗」を修正していくのが最良の方法です。
以下のコードで、コマンドライン版テストランナーのスイッチの一覧を見てみましょう。
phpunit --help
PHPUnit 3.7.0 by Sebastian Bergmann.
Usage: phpunit [switches] UnitTest [UnitTest.php]
phpunit [switches] <directory>
--log-junit <file> Log test execution in JUnit XML format to file.
--log-tap <file> Log test execution in TAP format to file.
--log-json <file> Log test execution in JSON format.
--coverage-clover <file> Generate code coverage report in Clover XML format.
--coverage-html <dir> Generate code coverage report in HTML format.
--coverage-php <file> Serialize PHP_CodeCoverage object to file.
--coverage-text=<file> Generate code coverage report in text format.
Default to writing to the standard output.
--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.
--testsuite <pattern> Filter which testsuite to run.
--group ... Only runs tests from the specified group(s).
--exclude-group ... Exclude tests from the specified group(s).
--list-groups List available test groups.
--test-suffix ... Only search for test in files with specified
suffix(es). Default: Test.php,.phpt
--loader <loader> TestSuiteLoader implementation to use.
--printer <printer> TestSuiteListener implementation to use.
--repeat <times> Runs the test(s) repeatedly.
--tap Report test execution progress in TAP format.
--testdox Report test execution progress in TestDox format.
--colors Use colors in output.
--stderr Write to STDERR instead of STDOUT.
--stop-on-error Stop execution upon first error.
--stop-on-failure Stop execution upon first error or failure.
--stop-on-skipped Stop execution upon first skipped test.
--stop-on-incomplete Stop execution upon first incomplete test.
--strict Run tests in strict mode.
-v|--verbose Output more verbose information.
--debug Display debugging information during test execution.
--process-isolation Run each test in a separate PHP process.
--no-globals-backup Do not backup and restore $GLOBALS for each test.
--static-backup Backup and restore static attributes for each test.
--bootstrap <file> A "bootstrap" PHP file that is run before the tests.
-c|--configuration <file> Read configuration from XML file.
--no-configuration Ignore default configuration file (phpunit.xml).
--include-path <path(s)> Prepend PHP's include_path with given path(s).
-d key[=value] Sets a php.ini value.
-h|--help Prints this usage information.
--version Prints the version and exits.phpunit UnitTest
UnitTest という名前のクラスで定義されている
テストを実行します。このクラスは、UnitTest.php
という名前のファイルの中に定義されているものとします。
UnitTest は、PHPUnit_Framework_TestCase
を継承したクラスであるか、あるいは PHPUnit_Framework_Test
オブジェクト、例えば PHPUnit_Framework_TestSuite
のインスタンスを返す public static suite()
というメソッドを保持するクラスでなければなりません。
phpunit UnitTest UnitTest.php
UnitTest という名前のクラスで定義されているテストを実行します。
このクラスは、指定したファイルの中で定義されているものとします。
--log-junitJUnit XML フォーマットを使用して、テストの実行結果のログを作成します。 詳細は 第 18 章 を参照ください。
--log-tapTest Anything Protocol (TAP) フォーマットを使用して、テストの実行結果のログを作成します。 詳細は 第 18 章 を参照ください。
--log-json--coverage-htmlコードカバレッジレポートを HTML 形式で作成します。詳細は 第 14 章 を参照ください。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--coverage-cloverテスト結果から XML 形式のログファイルを作成し、 コードカバレッジ情報もそこに含めます。 詳細は 第 18 章 を参照ください。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--coverage-phpシリアライズした PHP_CodeCoverage オブジェクトを生成し、 コードカバレッジ情報もそこに含めます。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--coverage-textテストを実行したときに、ログファイルあるいはコマンドライン出力で 可読形式のコードカバレッジ情報を生成します。 詳細は 第 18 章 を参照ください。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--testdox-html and --testdox-text実行したテストについて、HTML あるいはプレーンテキスト形式のドキュメントを生成します 詳細は 第 15 章 を参照ください。
--filter指定したパターンにマッチする名前のテストのみを実行します。 パターンとして指定できるのは、単一のテスト名か、 あるいは複数のテスト名にマッチする 正規表現 です。
--testsuite指定したパターンにマッチする名前のテストスイートのみを実行します。
--group
指定したグループのテストのみを実行します。
あるテストを特定のグループに所属させるには、
@group アノテーションを使用します。
@author アノテーションは
@group のエイリアスで、
テストの作者に基づいてテストをフィルタリングします。
--exclude-group
指定したグループをテストの対象外とします。
あるテストを特定のグループに所属させるには、
@group アノテーションを使用します。
--list-groups使用可能なテストグループの一覧を表示します。
--test-suffix指定したサフィックスのテストファイルだけを探します。
--loader
PHPUnit_Runner_TestSuiteLoader を実装したクラスのうち、
実際に使用するものを指定します。
標準のテストスイートローダーは、現在の作業ディレクトリおよび PHP
の設定項目 include_path
で指定されているディレクトリからソースファイルを探します。
PEAR の命名規則に従い、Project_Package_Class
クラスがソースファイル Project/Package/Class.php
に対応します。
--printer
結果を表示するために使うプリンタクラスを指定します。このプリンタクラスは
PHPUnit_Util_Printer を継承し、かつ
PHPUnit_Framework_TestListener
インターフェイスを実装したものでなければなりません。
--repeat指定された回数だけ、繰り返しテストを実行します。
--tapTest Anything Protocol (TAP) を使用して、テストの進行状況を報告します。 詳細は 第 18 章 を参照ください。
--testdoxテストの進行状況を、アジャイルな文書として報告します。 詳細は 第 15 章 を参照ください。
--colors出力に色を使用します。
--stderr
オプションで、出力先を
STDOUT ではなく STDERR にします。
--stop-on-error最初にエラーが発生した時点で実行を停止します。
--stop-on-failure最初にエラーあるいは失敗が発生した時点で実行を停止します。
--stop-on-skipped最初にテストのスキップが発生した時点で実行を停止します。
--stop-on-incomplete最初に不完全なテストがあらわれた時点で実行を停止します。
--strictテストを strict モードで実行します。
--verboseより詳細な情報を出力します。例えば、 未完成のテストや省略したテストの名前が表示されます。
--process-isolation各テストを個別の PHP プロセスで実行します。
--no-globals-backup$GLOBALS のバックアップ・リストアを行いません。 詳細は 「グローバルな状態」 を参照ください。
--static-backupユーザ定義クラスの静的属性のバックアップ・リストアを行います。 詳細は 「グローバルな状態」 を参照ください。
--bootstrapテストの前に実行される "ブートストラップ" PHP ファイルを指定します。
--configuration, -c設定を XML ファイルから読み込みます。 詳細は 付録 C を参照ください。
phpunit.xml あるいは
phpunit.xml.dist (この順番で使用します)
が現在の作業ディレクトリに存在しており、かつ --configuration
が使われていない場合、設定が自動的にそのファイルから読み込まれます。
--no-configuration
現在の作業ディレクトリにある phpunit.xml および
phpunit.xml.dist を無視します。
--include-path
PHP の include_path の先頭に、指定したパスを追加します。
-d指定した PHP 設定オプションの値を設定します。
--debugテスト名などのデバッグ情報を、テストの実行開始時に出力します。
テストを記述する際にいちばん時間を食うのは、テストを開始するための事前設定と テスト終了後の後始末の処理を書くことです。この事前設定は、テストの フィクスチャ と呼ばれます。
例 4.1
では、フィクスチャは
$fixture という変数に格納された配列だけでした。
しかし、たいていの場合はフィクスチャはこれより複雑なものとなり、
それを準備するにはかなりの量のコードが必要です。本来のテストの内容が、
フィクスチャを設定するためのコードの中に埋もれてしまうことになります。
この問題は、複数のテストで同じようなフィクスチャを設定する場合により顕著になります。
テストフレームワークの助けがなければ、
個々のテストのなかで同じような準備コードを繰り返し書くはめになってしまいます。
PHPUnit は、準備用のコードの共有をサポートしています。
各テストメソッドが実行される前に、setUp()
という名前のテンプレートメソッドが実行されます。setUp()
は、テスト対象のオブジェクトを生成するような処理に使用します。
テストメソッドの実行が終了すると、それが成功したか否かにかかわらず、
tearDown() という名前の別のテンプレートメソッドが実行されます。
tearDown() では、テスト対象のオブジェクトの後始末などを行います。
例 4.2
producer-consumer の関係を使って複数のテストでフィクスチャを共有しました。
これは常に要求されるというものではありませんし、常に可能だとも限りません。
例 6.1 では、
フィクスチャを再利用するのではなくコードで作成する方式で
StackTest にテストを書く方法をごらんいただきましょう。
まずインスタンス変数 $stack を宣言し、
メソッドローカル変数のかわりにこれを使うことにします。
そして、array の作成を
setUp() メソッドで行います。
最後に、冗長なコードをテストメソッドから削除し、
アサーションメソッド assertEquals()
ではメソッド変数 $stack のかわりに
新たに導入したインスタンス変数 $this->stack
を使うようにします。
例 6.1: setUp() を使用して stack フィクスチャを作成する
<?php
class StackTest extends PHPUnit_Framework_TestCase
{
protected $stack;
protected function setUp()
{
$this->stack = array();
}
public function testEmpty()
{
$this->assertTrue(empty($this->stack));
}
public function testPush()
{
array_push($this->stack, 'foo');
$this->assertEquals('foo', $this->stack[count($this->stack)-1]);
$this->assertFalse(empty($this->stack));
}
public function testPop()
{
array_push($this->stack, 'foo');
$this->assertEquals('foo', array_pop($this->stack));
$this->assertTrue(empty($this->stack));
}
}
?>
テンプレートメソッド setUp() および tearDown()
は、テストケースクラスのテストメソッドごとに (そして最初にインスタンスを作成したときに)
一度ずつ実行されます。
さらに、テンプレートメソッド setUpBeforeClass() および
tearDownAfterClass() が存在します。
これらはそれぞれ、テストケースクラスの最初のテストメソッドの実行前と
テストケースクラスの最後のテストの実行後にコールされます。
以下の例は、テストケースクラスで使用できるすべてのテンプレートメソッドを示すものです。
例 6.2: 利用可能なすべてのテンプレートメソッド
<?php
class TemplateMethodsTest extends PHPUnit_Framework_TestCase
{
public static function setUpBeforeClass()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function setUp()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function assertPreConditions()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public function testOne()
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(TRUE);
}
public function testTwo()
{
fwrite(STDOUT, __METHOD__ . "\n");
$this->assertTrue(FALSE);
}
protected function assertPostConditions()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function tearDown()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
public static function tearDownAfterClass()
{
fwrite(STDOUT, __METHOD__ . "\n");
}
protected function onNotSuccessfulTest(Exception $e)
{
fwrite(STDOUT, __METHOD__ . "\n");
throw $e;
}
}
?>
phpunit TemplateMethodsTest
PHPUnit 3.7.0 by Sebastian Bergmann.
TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass
Time: 0 seconds, Memory: 5.25Mb
There was 1 failure:
1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
setUp() と tearDown()
は理屈上では対称的になるはずですが、実際にはそうではありません。実際には、
tearDown() を実装する必要があるのは setUp()
で外部リソース (ファイルやソケットなど) を割り当てた場合のみです。もし
setUp() で単に PHP オブジェクトを作成しただけの場合は、
一般には tearDown() は必要ありません。しかし、もし
setUp() で大量のオブジェクトを作成した場合には、
それらの後始末をするために tearDown() で変数を
unset() したくなることもあるでしょう。
テストケースオブジェクト自体のガベージコレクションにはあまり意味がありません。
ふたつのテストがあって、それぞれの setup がほんの少しだけ違う場合にはどうなるでしょう? このような場合は、二種類の可能性が考えられます。
もし setUp() の違いがごくわずかなものなら、
その違う部分を setUp()
からテストメソッドのほうに移動させます。
setUp() の違いが大きければ、
テストケースクラスを別に分ける必要があります。それぞれのクラスには、
setup の違いを表す名前をつけます。
複数のテストの間でフィクスチャを共有する利点は、ほとんどありません。 しかし、設計上の問題などでどうしても フィクスチャを共有しなければならないこともあるでしょう。
複数のテスト間で共有する意味のあるフィクスチャの例として意味のあるものといえば、 データベースとの接続でしょう。テストのたびに新しいデータベース接続を毎回作成するのではなく、 最初にログインした状態を再利用するということです。こうすることで、 テストの実行時間を短縮できます。
例 6.3
では、テンプレートメソッド setUpBeforeClass() および
tearDownAfterClass() を用いて、
テストケースクラス内の最初のテストを実行する前にデータベースに接続し、
最後のテストが終わってから接続を切断するようにしています。
例 6.3: テストスイートの複数テスト間でのフィクスチャの共有
<?php
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected static $dbh;
public static function setUpBeforeClass()
{
self::$dbh = new PDO('sqlite::memory:');
}
public static function tearDownAfterClass()
{
self::$dbh = NULL;
}
}
?>
このようにフィクスチャを共有することがテストの価値を下げてしまうということを、 まだうまく伝え切れていないかもしれません。問題なのは、 各オブジェクトが疎結合になっていないという設計なのです。 複数が連携しているようなテストを作って設計上の問題から目をそらしてしまうのではなく、 きちんと設計しなおした上で、スタブ (第 10 章 を参照ください) を使用するテストを書くことをお勧めします。
singleton を使用するコードをテストするのはたいへんです。 同様に、グローバル変数を使うコードのテストもまたたいへんです。 一般に、テスト対象のコードはグローバル変数と密接に関連しており、 グローバル変数の内容を制御することはできません。 さらに別の問題もあって、あるテストの中でグローバル変数を変更してしまうと 別のテストがうまく動かなくなる可能性があります。
PHP では、グローバル変数は次のような動きをします。
グローバル変数 $foo = 'bar'; は、$GLOBALS['foo'] = 'bar'; として格納される。
$GLOBALS はスーパーグローバル変数と呼ばれる。
スーパーグローバル変数は組み込みの変数で、すべてのスコープで常に利用できる。
関数やメソッドのスコープでグローバル変数 $foo にアクセスするには、直接 $GLOBALS['foo'] にアクセスするか、あるいは global $foo; を用いて (グローバル変数を参照する) ローカル変数を作成する。
グローバル変数のほかに、クラスの静的属性もグローバル状態となります。
デフォルトでは、PHPUnit がテストを実行する際には、
グローバル変数やスーパーグローバル変数 ($GLOBALS,
$_ENV, $_POST,
$_GET, $_COOKIE,
$_SERVER, $_FILES,
$_REQUEST) への変更が他のテストへの影響を及ぼさないようにします。
オプションで、この分離をクラスの静的属性まで拡張することもできます。
クラスの静的属性のバックアップ・リストアの実装には、PHP 5.3 (あるいはそれ以降のバージョン) が必要です。
グローバル変数やクラスの静的属性のバックアップ・リストアの実装には
serialize() および
unserialize() を使用しています。
PHP 組み込みの一部のクラス、たとえば PDO
のオブジェクトはシリアライズできないため、そのようなオブジェクトが
$GLOBALS 配列に格納されている場合はバックアップ操作が失敗します。
「@backupGlobals」 で説明している
@backupGlobals アノテーションを使用すると、
グローバル変数のバックアップ・リストア操作を制御することができます。
あるいは、グローバル変数のブラックリストを指定して、
その変数だけはバックアップ・リストアの対象から除外することもできます。
class MyTest extends PHPUnit_Framework_TestCase
{
protected $backupGlobalsBlacklist = array('globalVariable');
// ...
}
$backupGlobalsBlacklist
属性をたとえば setUp()
メソッド内で設定しても効果が及ばないことに注意しましょう。
「@backupStaticAttributes」
で説明する @backupStaticAttributes アノテーションを使って、
静的属性の保存と復元を制御することができます。また、静的属性のブラックリストを渡せば
保存と復元の対象からそれらを除外することもできます。
ブラックリストは、このように指定します。
class MyTest extends PHPUnit_Framework_TestCase
{
protected $backupStaticAttributesBlacklist = array(
'className' => array('attributeName')
);
// ...
}
$backupStaticAttributesBlacklist
属性をたとえば setUp()
メソッド内で設定しても効果が及ばないことに注意しましょう。
PHPUnit の目指すところ (第 2 章 を参照ください) のひとつに 「自由に組み合わせられる」ということがあります。つまり、 例えば「そのプロジェクトのすべてのテストを実行する」「プロジェクトの中の ある部品を構成するすべてのクラスについて、すべてのテストを実行する」 「特定のひとつのクラスのテストのみを実行する」など、 数や組み合わせにとらわれずに好きなテストを一緒に実行できるということです。
PHPUnit では、さまざまな方法でテストを組み合わせてテストスイートにまとめることができます。 本章では、その中でもよく使われる手法を説明します。
おそらく、テストスイートをとりまとめるもっとも簡単な方法は すべてのテストケースのソースファイルを一つのテストディレクトリにまとめることでしょう。 PHPUnit はテストディレクトリを再帰的に探索し、 テストを自動的に見つけて実行します。
Object_Freezer
ライブラリのテストスイートを見てみましょう。このプロジェクトのディレクトリ構成を見ると、
テストケースクラスが Tests ディレクトリにまとめられていることがわかります。
その中のディレクトリの構造は、テスト対象のシステム (SUT) がある
Object ディレクトリ以下の構造と同じになっています。
Object Tests |-- Freezer |-- Freezer | |-- HashGenerator | |-- HashGenerator | | `-- NonRecursiveSHA1.php | | `-- NonRecursiveSHA1Test.php | |-- HashGenerator.php | | | |-- IdGenerator | |-- IdGenerator | | `-- UUID.php | | `-- UUIDTest.php | |-- IdGenerator.php | | | |-- LazyProxy.php | | | |-- Storage | |-- Storage | | `-- CouchDB.php | | `-- CouchDB | | | | |-- WithLazyLoadTest.php | | | | `-- WithoutLazyLoadTest.php | |-- Storage.php | |-- StorageTest.php | `-- Util.php | `-- UtilTest.php `-- Freezer.php `-- FreezerTest.php
PHPUnit のコマンドラインテストランナーに テストディレクトリの場所を指示してやるだけで、 このライブラリのすべてのテストを実行できます。
phpunit Tests
PHPUnit 3.7.0 by Sebastian Bergmann.
............................................................ 60 / 75
...............
Time: 0 seconds
OK (75 tests, 164 assertions)
PHPUnit のコマンドラインテストランナーでディレクトリを指定すると、
その中の *Test.php ファイルを見つけて実行します。
Tests/FreezerTest.php にあるテストケースクラス
Object_FreezerTest で宣言されているテストだけを実行するには、
次のコマンドを実行します。
phpunit Tests/FreezerTest
PHPUnit 3.7.0 by Sebastian Bergmann.
............................
Time: 0 seconds
OK (28 tests, 60 assertions)
実行したいテストをより細かく指示するには
--filter スイッチを使います。
phpunit --filter testFreezingAnObjectWorks Tests
PHPUnit 3.7.0 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 2 assertions)この方式の欠点は、テストの実行順を制御できないことです。 そのため、テストの依存性に関する問題を引き起こすことがあります。 「テストの依存性」 を参照ください。 次の節では、テストの実行順序を XML 設定ファイルで明示的に指定する方法を説明します。
PHPUnit の XML 設定ファイル (付録 C)
を使ってテストスイートを構成することもできます。
例 7.1
に、最小限の例を示します。これは、
Tests を再帰的に探索して
*Test.php というファイルにある
*Test クラスをすべて追加する設定です。
例 7.1: XML 設定ファイルを用いたテストスイートの構成
<phpunit>
<testsuites>
<testsuite name="Object_Freezer">
<directory>Tests</directory>
</testsuite>
</testsuites>
</phpunit>どのテストを実行するのかを明示的に指定することができます。
例 7.2: XML 設定ファイルを用いたテストスイートの構成
<phpunit>
<testsuites>
<testsuite name="Object_Freezer">
<file>Tests/Freezer/HashGenerator/NonRecursiveSHA1Test.php</file>
<file>Tests/Freezer/IdGenerator/UUIDTest.php</file>
<file>Tests/Freezer/UtilTest.php</file>
<file>Tests/FreezerTest.php</file>
<file>Tests/Freezer/StorageTest.php</file>
<file>Tests/Freezer/Storage/CouchDB/WithLazyLoadTest.php</file>
<file>Tests/Freezer/Storage/CouchDB/WithoutLazyLoadTest.php</file>
</testsuite>
</testsuites>
</phpunit>初級者・中級者向けのユニットテストのサンプルは、 どんな言語を対象としたものであっても、 テストしやすいようなロジックに対してシンプルなテストをしているものばかりです。 データベースを扱う一般的なアプリケーションを考えると、これはまったく現実離れしています。 たとえば Wordpress や TYPO3、あるいは Symfony で Doctrine や Propel などを使い始めるとすぐに、 PHPUnit でのテストがやりづらいことを実感するはずです。 データベースとこれらのライブラリが密結合になっているからです。
きっと日々の業務やプロジェクトでも身に覚えがあることでしょう。 自分の持つ PHPUnit に関する知識を駆使して作業を進めようとしたのに、 こんな問題のせいで行き詰ってしまうことが。
テストしたいメソッドがかなり大きめの JOIN 操作を実行し、 データを使って重要な結果を算出している。
ひとつのビジネスロジックの中で SELECT、INSERT、UPDATE そして DELETE を組み合わせて実行している。
ふたつ以上の (おそらくもっと多い) テーブルから初期データを準備しないと そのメソッドのテストができない。
DBUnit 拡張を使うと、テスト用のデータベースのセットアップを単純化でき、 データベース操作後の内容の検証もすることができます。次のようにインストールします。
pear install phpunit/DbUnitDBUnit が現在サポートしているのは、MySQL および PostgreSQL、Oracle、SQLite です。 Zend Framework や Doctrine 2 を使うと、IBM DB2 や Microsoft SQL Server のような他のデータベースにもアクセスできます。
ウェブ上にあるユニットテストのサンプルの中にデータベースを扱うものが全く見当たらない理由はなぜか。 それは、データベースを扱うテストは準備するのも保守するのもたいへんだからです。 データベースを使うテストをするには、このようなことに気をつける必要があります。
データベースのスキーマやテーブル
テーブルへの、テストで必要となるレコードの追加
テスト実行後のデータベースの状態の検証
テスト実行ごとのデータベースの後始末
PDO や MySQLi あるいは OCI8 といったデータベース API はどれも使いにくい上に、 こういった処理を自分で書こうとすると長ったらしくなってしまって面倒です。
テストコードはできる限り簡潔に、そして明確に書かねばなりません。その理由は次のとおりです。
製品コードにちょっと手を加えるたびに大量のテストコードを変更する羽目になるのは困る。
数ヵ月後に改めて読み直したときにも 読みやすく理解しやすいテストコードであってほしい。
さらに知っておく必要があることは、 データベースは基本的に、自分のコードへのグローバルな入力変数であるということです。 テストスイート内にあるふたつのテストを同じデータベースに対して実行すると、 おそらくデータを複数回再利用することになります。あるテストが失敗すると それ以降のテストの結果にも影響を及ぼしやすく、テストを進めるのが非常に難しくなります。 先ほど箇条書きでまとめた中の「後始末」こそが、この 「データベースがグローバルな入力になる」 問題を解決するために重要です。
DbUnit を使うと、 データベースのテストにおけるこれらの問題をシンプルにする助けになります。
PHPUnit では助けようにもどうにもならないことが、 データベースのテストはデータベースを使わないものに比べてとても遅くなるという事実です。 テストの実行時間がどれくらいになるかはデータベースとのやりとりの量に依存しますが、 各テストで使うデータの量を少なめにしておいて 可能な限りはデータベースを使わないテストで済ませるようにすれば、 巨大なテストスイートであっても 1 分未満で実行させるのは容易です。
Doctrine 2 プロジェクト がよい例です。 このプロジェクトのテストスイートには現時点で約 1000 件のテストが含まれています。 そのほぼ半数がデータベースを扱うテストですが、 標準的なデスクトップコンピューター上の MySQL を使ってテストスイートを実行しても 15 秒程度でテストが完了します。
Gerard Meszaros は、著書 xUnit Test Patterns でユニットテストを次の四段階に分類しています。
フィクスチャのセットアップ (Setup)
テストしたいシステムの実行 (Exercise)
結果の検証 (Verify)
後始末 (Teardown)
フィクスチャとは?
フィクスチャとは、アプリケーションやデータベースの初期状態のことです。 テストを実行する前に用意します。
データベースをテストするには、少なくとも setup と teardown のときにはテーブルに接続してフィクスチャのクリーンアップや書き込みをしなければなりません。 しかし、データベース拡張には、 データベーステストの四段階を次のようなワークフローに振り向ける十分な理由があります。 このフローは、個々のテストに対して実行します。
データベースを扱う最初のテストというのはいつでも存在します。 実際のところ、そのときテーブルにデータが存在するのかどうかはわかりません。 PHPUnit は指定した全テーブルに対して TRUNCATE を実行し、 テーブルの中身を空にします。
通常、PHPUnit を使うテストケースでは
PHPUnit_Framework_TestCase
クラスを継承してこのようにします。
require_once "PHPUnit/Framework/TestCase.php";
class MyTest extends PHPUnit_Framework_TestCase
{
public function testCalculate()
{
$this->assertEquals(2, 1 + 1);
}
}
テストコードで Database Extension を使う場合は少しだけ複雑になり、
別の抽象テストケースを継承しなければなりません。そして、二つの抽象メソッド
getConnection() と
getDataSet() を実装します。
require_once "PHPUnit/Extensions/Database/TestCase.php";
class MyGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
/**
* @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
*/
public function getConnection()
{
$pdo = new PDO('sqlite::memory:');
return $this->createDefaultDBConnection($pdo, ':memory:');
}
/**
* @return PHPUnit_Extensions_Database_DataSet_IDataSet
*/
public function getDataSet()
{
return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-seed.xml');
}
}
クリーンアップとフィクスチャの読み込みの機能を動かすには、 PHPUnit Database Extension からデータベース接続にアクセスできなければなりません。 データベース接続の抽象化には PDO ライブラリを使います。 重要なのは、PHPUnit のデータベース拡張を使うためだけに わざわざアプリケーションを PDO ベースにする必要はないということです。 この接続を使うのは、単にクリーンアップとフィクスチャの準備のためだけです。
先ほどの例では、インメモリの SQLite 接続を作って
createDefaultDBConnection メソッドに渡しました。
このメソッドは PDO のインスタンスをラップしたもので、二番目のパラメータ
(データベース名) に非常にシンプルなデータベース接続の抽象化レイヤーを渡します。このパラメータの型は
PHPUnit_Extensions_Database_DB_IDatabaseConnection です。
「データベース接続の使い方」で、このインターフェイスの API と、その活用法について説明します。
getDataSet() メソッドで定義するのは、
個々のテストを実行する前のデータベースの初期状態がどうあるべきかということです。
データベースの状態の抽象化は DataSet と DataTable
という概念を使って行い、これらをそれぞれ
PHPUnit_Extensions_Database_DataSet_IDataSet および
PHPUnit_Extensions_Database_DataSet_IDataTable
というインターフェイスで表します。次の節でこれらの概念を詳しく説明し、
これをデータベースのテストに使うと何がうれしいのかについても示します。
実装するために最低限知っておくべきことは、
getDataSet() メソッドがコールされるのが
setUp() の中で一度だけであり、
ここでフィクスチャのデータセットを取得してデータベースに挿入するということです。
先ほどの例では、ファクトリメソッド
createFlatXMLDataSet($filename)
を使って XML 形式のデータセットを表しました。
PHPUnit は、テストの実行前にデータベーススキーマ (すべてのテーブル、トリガー、シーケンス、ビューを含むもの) ができあがっていることを想定しています。つまり開発者としては、 テストスイートを実行する前にデータベースを正しく準備しておかねばならないということです。
データベースのテストにおけるこの事前条件を満たす方法には、次のようなものがあります。
インメモリの SQLite ではなく永続化したデータベースを使うのなら、 最初に一度 phpMyAdmin (MySQL の場合) などのツールでデータベースを用意しておけば、 あとはテストを実行するたびにそれを再利用できます。
Doctrine 2 や Propel といったライブラリを使っている場合は、その API を使えばテストの実行前に必要なデータベーススキーマを作ることができます。 PHPUnit のブートストラップ 機能を使うと、そのコードをテスト実行時に毎回実行させることもできます。
先の実装例を見ればすぐにわかるでしょうが、
getConnection() メソッドはきわめて静的なものであり、
さまざまなデータベーステストケースで再利用することができます。
さらに、テストのパフォーマンスを良好に保ちつつデータベースのオーバーヘッドを下げるために、
ちょっとしたリファクタリングを施して汎用的な抽象テストケースを用意しましょう。
このようにしても、テストケースごとに異なるデータフィクスチャを指定することができます。
require_once "PHPUnit/Extensions/Database/TestCase.php";
abstract class MyApp_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
// PDO のインスタンス生成は、クリーンアップおよびフィクスチャ読み込みのときに一度だけ
static private $pdo = null;
// PHPUnit_Extensions_Database_DB_IDatabaseConnection のインスタンス生成は、テストごとに一度だけ
private $conn = null;
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO('sqlite::memory:');
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, ':memory:');
}
return $this->conn;
}
}
しかし、これはまだデータベースへの接続情報を PDO 接続の設定にハードコードしてしまっています。 PHPUnit にはさらにすばらしい機能があるので、それを使ってテストケースをより汎用的にしましょう。 XML 設定ファイル を使えば、テストの実行のたびにデータベース接続を設定できます。 まずは 「phpunit.xml」 というファイルをアプリケーションの tests/ ディレクトリに作り、 中身をこのようにします。
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
<php>
<var name="DB_DSN" value="mysql:dbname=myguestbook;host=localhost" />
<var name="DB_USER" value="user" />
<var name="DB_PASSWD" value="passwd" />
<var name="DB_DBNAME" value="myguestbook" />
</php>
</phpunit>
テストケースはこのように書き直せます。
abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
// PDO のインスタンス生成は、クリーンアップおよびフィクスチャ読み込みのときに一度だけ
static private $pdo = null;
// PHPUnit_Extensions_Database_DB_IDatabaseConnection のインスタンス生成は、テストごとに一度だけ
private $conn = null;
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] );
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
}
return $this->conn;
}
}
データベースの設定情報を切り替えてテストスイートを実行するには、 コマンドラインから次のようにします。
user@desktop> phpunit --configuration developer-a.xml MyTests/ user@desktop> phpunit --configuration developer-b.xml MyTests/
データベースのテストを実行するときにターゲットデータベースを切り替えられるようにしておくことは、 開発機で作業をしている場合などは特に重要です。 複数の開発者が同じデータベース接続を使ってデータベースのテストを実行したりすると、 レースコンディション (競合条件) によるテストの失敗が頻発するでしょう。
PHPUnit Database Extension の中心となる概念が データセットとデータテーブルです。まずはこの考え方を理解することが、 PHPUnit でのデータベースのテストをマスターする近道です。 データセットとデータテーブルは、データベースのテーブルや行、 そしてカラムの抽象化レイヤーです。シンプルな API によってデータベースの内容をオブジェクト構造に隠蔽できるだけでなく、 データベース以外のソースによる実装もできるようになっています。
この抽象化を使って、データベースの実際の中身と我々が期待する内容を比較します。 期待する内容は XML や YAML そして CSV などのファイルでも表せますし、 PHP の配列として表すこともできます。 DataSet インターフェイスと DataTable インターフェイスのおかげで、 これらの全く異なる概念のソースをリレーショナルデータベースに見立てて 同様に扱えるようになります。
データベースのアサーションをテストの中で行う流れは、 次のようにシンプルな三段階となります。
ひとつあるいは複数のテーブルをデータベース内から指定する (実際のデータセット)。
期待するデータセットをお好みのフォーマット (YAML, XML など) で用意する。
両者がお互いに等しいことを確認する。
データセットやデータテーブルの PHPUnit Database Extension における使い道は、 何もアサーションだけだというわけではありません。先ほどの節で見たように、 これらを使ってデータベースの初期状態の内容を記述することもできます。 フィクスチャとなるデータセットを Database TestCase で定義すると、それをこのように使うことができます。
データセットで指定したテーブルのすべての行を削除する。
データテーブルのすべての行をデータベースに書き込む。
これら三種類のデータセット/データテーブルが用意されています。
ファイルベースのデータセットやデータテーブル
クエリベースのデータセットやデータテーブル
フィルタ用や合成用のデータセットやデータテーブル
ファイルベースのデータセットやデータテーブルは、 初期状態のフィクスチャを定義したり期待する状態を定義したりするときによく使います。
最も一般的なデータセットは、フラット XML と呼ばれるものです。
これは非常にシンプルな xml 形式で、ルートノード
<dataset>
の中のタグがデータベースのひとつの行を表します。
テーブルと同じ名前のタグが追加する行を表し、
その属性がカラムを表します。
単純な掲示板アプリケーションの例は、このようになります。
<?xml version="1.0" ?>
<dataset>
<guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
<guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
</dataset>
見るからに書きやすそうですね。この場合は
<guestbook> がテーブル名で、
2 行が追加されます。そして、四つのカラム 「id」、
「content」、「user」 そして
「created」 に、それぞれ対応する値が設定されています。
しかし、この単純性による問題もあります。
たとえば、先ほどの例で空のテーブルはどうやって指定すればいいのかがよくわかりません。 実は、何も属性を指定せずにテーブルと同じ名前のタグを追加すれば、空のテーブルを表すことができます。 空の guestbook テーブルを表すフラット xml ファイルは、このようになります。
<?xml version="1.0" ?>
<dataset>
<guestbook />
</dataset>
フラット xml データセットでの NULL 値の処理は、あまりおもしろいものではありません。 ほとんどのデータベースでは、NULL 値と空文字列は別のものとして扱います (例外のひとつは Oracle です) が、これをフラット xml 形式で表すのは困難です。NULL 値を表すには、 行の指定のときに属性を省略します。 この例の掲示板で、匿名の投稿を許可し、そのときには user カラムに NULL を指定することにしましょう。 guestbook テーブルの状態は、このようになります。
<?xml version="1.0" ?>
<dataset>
<guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
<guestbook id="2" content="I like it!" created="2010-04-26 12:14:20" />
</dataset>
この例では、二番目のエントリが匿名の投稿を表します。 しかし、これはカラムの認識において深刻な問題につながります。 データセットが等しいことを確認するアサーションでは、各データセットでテーブルの持つカラムを指定しなければなりません。 ある属性がデータテーブルのすべての行で NULL だったなら、 Database Extension はそのカラムがテーブルに存在することをどうやって知るというのでしょう?
フラット XML データセットはここで、重大な前提を使っています。 テーブルの最初の行で定義されている属性が、そのテーブルのカラムを定義しているものと見なすのです。 先ほどの例では、guestbook テーブルのカラムが 「id」 と 「content」、「user」 そして 「created」 であると見なすということです。二番目の行には 「user」 が定義されていないので、データベースには NULL を挿入します。
guestbook の最初のエントリをデータセットから削除すると、guestbook テーブルのカラムは 「id」、「content」 そして 「created」 だけになってしまいます。 「user」 が指定されていないからです。
フラット XML データセットを効率的に使うには、NULL 値がからむ場合は 各テーブルの最初の行には NULL を含まないようにします。 それ以降の行では、属性を省略して NULL を表すことができます。 これはあまりスマートなやり方ではありません。 というのも、データベースのアサーションで行の順番が影響してしまうからです。
一方、テーブルのカラムの一部だけをフラット XML データセットで指定すると、 それ以外のカラムにはデフォルト値が設定されます。 そのため、もし省略したカラムの定義が 「NOT NULL DEFAULT NULL」 などの場合はエラーになります。
結論として言えるのは、フラット XML データセットを使うなら NULL 値が不要な場合だけにしておいたほうがよい、ということだけです。
フラット XML データセットのインスタンスを
Database TestCase から作るには、
createFlatXmlDataSet($filename) メソッドを使います。
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createFlatXmlDataSet('myFlatXmlFixture.yml');
}
}
もうひとつ別の構造の XML データセットもあります。これは多少冗長な書き方ですが、
フラット XML データセットにおける NULL の問題は発生しません。
ルートノード <dataset> の配下に指定できるタグは、
<table> や
<column>、<row>、
<value> そして
<null /> です。
先に定義した Guestbook のフラット XML と同様のデータセットは、このようになります。
<?xml version="1.0" ?>
<dataset>
<table name="guestbook">
<column>id</column>
<column>content</column>
<column>user</column>
<column>created</column>
<row>
<value>1</value>
<value>Hello buddy!</value>
<value>joe</value>
<value>2010-04-24 17:15:23</value>
</row>
<row>
<value>2</value>
<value>I like it!</value>
<null />
<value>2010-04-26 12:14:20</value>
</row>
</table>
</dataset>
<table> には name が必須で、
さらにすべてのカラムの名前を定義しなければなりません。
また、ゼロ個以上の <row>
要素を含めることができます。<row>
要素を定義しなければ、そのテーブルが空であることになります。
<value> タグや
<null /> タグは、先に指定した
<column> 要素の順番で指定しなければなりません。
<null /> タグは、
見た目の通り、値が NULL であることを表します。
XML データセットのインスタンスを
Database TestCase から作るには、
createXmlDataSet($filename) メソッドを使います。
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createXMLDataSet('myXmlFixture.xml');
}
}
この新しい XML フォーマットは、
MySQL データベース 専用です。
PHPUnit 3.5 以降で対応します。この形式のファイルを生成するには、
mysqldump
を使います。mysqldump では CSV データセットも対応していますが、
それとは違ってこの XML 形式の場合はひとつのファイルに複数のテーブルを含めることができます。
この形式のファイルを作るには、
mysqldump を次のように実行します。
mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml
このファイルを Database TestCase で使うには、
createMySQLXMLDataSet($filename) メソッドをコールします。
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createMySQLXMLDataSet('/path/to/file.xml');
}
}
PHPUnit 3.4 以降では、データセットを YAML 形式でも指定できるようになりました。 この機能を使うには、PHPUnit 3.4 以降を PEAR からインストールするときに、 依存パッケージ Symfony/YAML もインストールしなければなりません。 guestbook の例を YAML データセットで表すと、このようになります。
guestbook:
-
id: 1
content: "Hello buddy!"
user: "joe"
created: 2010-04-24 17:15:23
-
id: 2
content: "I like it!"
user:
created: 2010-04-26 12:14:20
これは、シンプルで便利なうえに、さらにフラット XML
データセットが持つ NULL の問題も解決しています。
NULL を YAML で表すには、単にカラム名の後に何も値を指定しなければよいのです。
空文字列を指定する場合は
column1: ""
のようにします。
YAML Dataset 用のファクトリーメソッドは今のところ Database TestCase に存在しないので、手動でインスタンスを生成しなければなりません。
require_once "PHPUnit/Extensions/Database/DataSet/YamlDataSet.php";
class YamlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getDataSet()
{
return new PHPUnit_Extensions_Database_DataSet_YamlDataSet(
dirname(__FILE__)."/_files/guestbook.yml"
);
}
}
さらにもうひとつのファイルベースのデータセットとして、CSV ファイルを使ったものもあります。データセット内の各テーブルを、 それぞれ単一の CSV ファイルとして扱います。 guestbook の例では、このようなファイル guestbook-table.csv を定義します。
id;content;user;created 1;"Hello buddy!";"joe";"2010-04-24 17:15:23" 2;"I like it!""nancy";"2010-04-26 12:14:20"
この形式は Excel や OpenOffice で編集できるという点で非常に便利ですが、 CSV データセットでは NULL 値を指定することができません。 空のカラムは、データベースのデフォルトに基づいた空の値として扱われます。
CSV データセットを作るには、このようにします。
require_once 'PHPUnit/Extensions/Database/DataSet/CsvDataSet.php';
class CsvGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getDataSet()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
$dataSet->addTable('guestbook', dirname(__FILE__)."/_files/guestbook.csv");
return $dataSet;
}
}
PHPUnit の Database Extension には、(今のところ) 配列ベースのデータセットが存在しません。しかし、自分で簡単に実装できます。 guestbook の例だと、このようになります。
class ArrayGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getDataSet()
{
return new MyApp_DbUnit_ArrayDataSet(array(
'guestbook' => array(
array('id' => 1, 'content' => 'Hello buddy!', 'user' => 'joe', 'created' => '2010-04-24 17:15:23'),
array('id' => 2, 'content' => 'I like it!', 'user' => null, 'created' => '2010-04-26 12:14:20'),
),
));
}
}
PHP の DataSet には、これまでのファイルベースのデータセットに比べて明らかな利点があります。
PHP の配列は NULL 値を扱える。
アサーション用に新たなファイルを用意する必要がなく、 直接テストケース内で指定できる。
このデータセットでは、フラット XML や CSV そして YAML データセットと同様に、最初に指定した行のキーがテーブルのカラム名を表します。 つまり、先ほどの例だと 「id」、 「content」、「user」 そして 「created」 です。
この Array データセットの実装は、シンプルで直感的なものです。
require_once 'PHPUnit/Util/Filter.php';
require_once 'PHPUnit/Extensions/Database/DataSet/AbstractDataSet.php';
require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTableIterator.php';
require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTable.php';
require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTableMetaData.php';
PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
class MyApp_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
{
/**
* @var array
*/
protected $tables = array();
/**
* @param array $data
*/
public function __construct(array $data)
{
foreach ($data AS $tableName => $rows) {
$columns = array();
if (isset($rows[0])) {
$columns = array_keys($rows[0]);
}
$metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
$table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
foreach ($rows AS $row) {
$table->addRow($row);
}
$this->tables[$tableName] = $table;
}
}
protected function createIterator($reverse = FALSE)
{
return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
}
public function getTable($tableName)
{
if (!isset($this->tables[$tableName])) {
throw new InvalidArgumentException("$tableName is not a table in the current database.");
}
return $this->tables[$tableName];
}
}
データベースのアサーションでは、ファイルベースのデータセットだけでなく Query/SQL ベースのデータセットでデータベースの実際の中身を含むものが必要になることもあります。 そんなときに使えるのが Query データセットです。
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook');
単にテーブル名だけを指定してテーブルを追加すると、 次のクエリを実行してデータテーブルを定義したのと同じ意味になります。
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT * FROM guestbook');
ここでテーブルに対して任意のクエリを実行して、
取得する行や列を絞り込んだり
ORDER BY 句を追加したりすることができます。
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT id, content FROM guestbook ORDER BY created DESC');
データベースアサーションの節で、このデータセットを使う方法をより詳しく説明しています。
テスト用のデータベース接続にアクセスすると、 自動的にすべてのテーブルとその中身を含むデータセットを生成します。 接続先のデータベースは、接続用のファクトリーメソッドの二番目のパラメータで指定します。
データベース全体の完全なデータセットを作るには
testGuestbook() のようにします。
ホワイトリスト形式で指定したテーブルだけに絞り込むには
testFilteredGuestbook() メソッドのようにします。
class MySqlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
/**
* @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
*/
public function getConnection()
{
$database = 'my_database';
$pdo = new PDO('mysql:...', $user, $password);
return $this->createDefaultDBConnection($pdo, $database);
}
public function testGuestbook()
{
$dataSet = $this->getConnection()->createDataSet();
// ...
}
public function testFilteredGuestbook()
{
$tableNames = array('guestbook');
$dataSet = $this->getConnection()->createDataSet($tableNames);
// ...
}
}
これまで、フラット XML や CSV のデータセットには NULL の問題があると説明してきました。 しかし、ちょっとわかりにくい回避策を使えばこれらのデータセットで NULL を扱うこともできます。
Replacement データセットは既存のデータセットに対するデコレータで、 データセットの任意のカラムの値を別の値で置換することができます。 guestbook の例で NULL 値を扱うには、このようなファイルを作ります。
<?xml version="1.0" ?>
<dataset>
<guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
<guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>
そして、フラット XML データセットを Replacement データセットでラップします。
require_once 'PHPUnit/Extensions/Database/DataSet/ReplacementDataSet.php';
class ReplacementTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
$ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
$rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
$rds->addFullReplacement('##NULL##', null);
return $rds;
}
}
巨大なフィクスチャファイルを扱うときには、 データセットフィルタをホワイトリストあるいはブラックリストとして使って テーブルやカラムを絞り込んだサブデータセットを作ることができます。 これは、DB データセットと組み合わせて データセットのカラムを絞り込むときに使うと非常に便利です。
class DataSetFilterTest extends PHPUnit_Extensions_Database_TestCase
{
public function testIncludeFilteredGuestbook()
{
$tableNames = array('guestbook');
$dataSet = $this->getConnection()->createDataSet();
$filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
$filterDataSet->addIncludeTables(array('guestbook'));
$filterDataSet->setIncludeColumnsForTable('guestbook', array('id', 'content'));
// ..
}
public function testExcludeFilteredGuestbook()
{
$tableNames = array('guestbook');
$dataSet = $this->getConnection()->createDataSet();
$filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
$filterDataSet->addExcludeTables(array('foo', 'bar', 'baz')); // guestbook だけをキープ!
$filterDataSet->setExcludeColumnsForTable('guestbook', array('user', 'created'));
// ..
}
}
注意 ひとつのテーブルに対してカラムの exclude フィルタと include フィルタを同時に使うことはできません。 さらに、テーブルのホワイトリストとブラックリストはどちらか一方しか指定できません。
Composite データセットは、既存の複数のデータセットをひとつにまとめるときに有用です。 複数のデータセットに同名のテーブルが含まれる場合は、 指定した順で行を連結します。 たとえば、このようなふたつのデータセットがあるものとしましょう。 まずは fixture1.xml。
<?xml version="1.0" ?>
<dataset>
<guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
</dataset>
そして fixture2.xml。
<?xml version="1.0" ?>
<dataset>
<guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>
Composite データセットを使えば、両方のフィクスチャファイルをまとめることができます。
class CompositeTest extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
$ds1 = $this->createFlatXmlDataSet('fixture1.xml');
$ds2 = $this->createFlatXmlDataSet('fixture2.xml');
$compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
$compositeDs->addDataSet($ds1);
$compositeDs->addDataSet($ds2);
return $compositeDs;
}
}
フィクスチャを準備するとき、PHPUnit の Database Extension はフィクスチャ内で定義された順に行を追加していきます。 データベースのスキーマ定義で外部キーを使っている場合は、 外部キー制約に違反しないような順番でテーブルを指定しなければなりません。
データセットやデータテーブルの内部構造を理解するために、 まずはデータセットのインターフェイスから見ていきましょう。 自分でデータセットやデータテーブルを作るつもりのない人は、 読み飛ばしてもかまいません。
interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate
{
public function getTableNames();
public function getTableMetaData($tableName);
public function getTable($tableName);
public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $other);
public function getReverseIterator();
}
公開インターフェイスは、データベーステストケースの
assertDataSetsEqual()
アサーションで内部的に使われており、これでデータセットの内容を検証します。
IDataSet は IteratorAggregate
インターフェイスから getIterator()
メソッドを継承しており、これを使ってデータセット内の全テーブルの反復処理を行います。
さらにリバースイテレータメソッドも必須です。
指定した順の逆順でのテーブルの切り詰めを、このメソッドで行います。
リバースイテレータがなぜ必要なのか、次のふたつのテーブル (TableA と TableB) を例にして考えましょう。TableB には TableA のカラムへの外部キーが定義されているものとします。 フィクスチャを準備するときには、まず TableA に行を追加してから依存レコードを TableB に追加します。当然、テーブルの中身をすべて削除するときには 逆順にして先に TableB から削除しないと外部キー制約に違反してしまいます。
テーブルのインスタンスをデータセットに追加するには、
実装によってさまざまな手法があります。たとえば
YamlDataSet や
XmlDataSet そして FlatXmlDataSet
のようなファイルベースのデータセットでは、
データセットの作成時にソースファイルを使って内部的に追加します。
テーブルは、このようなインターフェイスを使って表します。
interface PHPUnit_Extensions_Database_DataSet_ITable
{
public function getTableMetaData();
public function getRowCount();
public function getValue($row, $column);
public function getRow($row);
public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $other);
}
getTableMetaData() メソッドは別として、
それ以外のメソッドはまさに文字通りの働きをするものです。
これらのメソッドはすべて、Database Extension のさまざまなアサーションで必須となります。
その詳細は次の章で説明します。
getTableMetaData() メソッドの返す値は、
PHPUnit_Extensions_Database_DataSet_ITableMetaData
インターフェイスを実装したものでなければなりません。
このインターフェイスはテーブルの構造を表し、このような情報を保持します。
テーブル名。
テーブルのカラム名の配列。並び順は、結果セットに登場する順と同じ。
主キーカラムの配列。
このインターフェイスには、ふたつの TableMetaData のインスタンスがお互いに等しいかを調べるアサーションも定義されています。 これは、データセットの同一性を調べるアサーションで利用するものです。
Connection インターフェイスには、三種類のおもしろいメソッドが用意されています。
このインターフェイスは、データベーステストケースの
getConnection() メソッドが返すものです。
interface PHPUnit_Extensions_Database_DB_IDatabaseConnection
{
public function createDataSet(Array $tableNames = NULL);
public function createQueryTable($resultName, $sql);
public function getRowCount($tableName, $whereClause = NULL);
// ...
}
createDataSet() メソッドは、Database
(DB) データセットを作ります。これは、データセットの実装の節で説明したものです。
class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
{
public function testCreateDataSet()
{
$tableNames = array('guestbook');
$dataSet = $this->getConnection()->createDataSet();
}
}
createQueryTable() メソッドを使うと、
QuryTable のインスタンスを作れます。引数には、結果の名前と SQL クエリを渡します。
これは、次の節 (データベースアサーション API)
で説明する結果やテーブルのアサーションで有用なメソッドです。
class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
{
public function testCreateQueryTable()
{
$tableNames = array('guestbook');
$queryTable = $this->getConnection()->createQueryTable('guestbook', 'SELECT * FROM guestbook');
}
}
getRowCount() は、
テーブル内の行数を手軽に取得するためのメソッドです。
オプションで、where 句によるフィルタリングもできます。
これを使えば、シンプルな同一性のアサーションが可能です。
class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
{
public function testGetRowCount()
{
$this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'));
}
}
テストツール用として、Database Extension ではいくつかのアサーションを提供しています。 これらを使えば、データベースやテーブルの現在の状態 そしてテーブルの行数を検証できます。この節では、 これらの機能の詳細を説明します。
テーブルの行数が特定の値であるかどうかを調べられれば便利なことがよくあります。 これは、接続 API を使ってちょっとしたコードを書かなくとも簡単に実現できます。 guestbook に行を追加した後で、初期登録した 2 エントリ以外にもう一行増えて 3 行になっていることを調べるには、このようにします。
class GuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
public function testAddEntry()
{
$this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'), "事前条件");
$guestbook = new Guestbook();
$guestbook->addEntry("suzy", "Hello world!");
$this->assertEquals(3, $this->getConnection()->getRowCount('guestbook'), "追加に失敗");
}
}
先ほどのアサーションも有用ですが、本当にチェックしたいのは、 すべての値が正しいカラムにきちんと登録されたかどうかです。 これは、テーブルのアサーションで実現します。
そのために、QueryTable のインスタンスを定義しました。 テーブル名と SQL クエリからその内容を取得し、 それをファイルベースあるいは配列ベースのデータセットと比較します。
class GuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
public function testAddEntry()
{
$guestbook = new Guestbook();
$guestbook->addEntry("suzy", "Hello world!");
$queryTable = $this->getConnection()->createQueryTable(
'guestbook', 'SELECT * FROM guestbook'
);
$expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
->getTable("guestbook");
$this->assertTablesEqual($expectedTable, $queryTable);
}
}
さて次に、このアサーションに使うフラット XML ファイル expectedBook.xml を用意しましょう。
<?xml version="1.0" ?>
<dataset>
<guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
<guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
<guestbook id="3" content="Hello world!" user="suzy" created="2010-05-01 21:47:08" />
</dataset>
残念ながら、このアサーションが成功するのは、ちょうど 2010–05–01 21:47:08 に実行したときだけになります。 日付はデータベースのテストでいつも問題になるものなので、それを回避する手段として 「created」 カラムをアサーションで無視させることができます。
調整後のフラット XML ファイル expectedBook.xml はこのようになり、これでアサーションを通過させることができます。
<?xml version="1.0" ?>
<dataset>
<guestbook id="1" content="Hello buddy!" user="joe" />
<guestbook id="2" content="I like it!" user="nancy" />
<guestbook id="3" content="Hello world!" user="suzy" />
</dataset>
QueryTable の呼び出しも修正しなければなりません。
$queryTable = $this->getConnection()->createQueryTable(
'guestbook', 'SELECT id, content, user FROM guestbook'
);
複雑なクエリの結果に対するアサーションも、 QueryTable 方式で可能です。単に結果の名前とクエリを指定して、 それをデータセットと比較すればよいのです。
class ComplexQueryTest extends PHPUnit_Extensions_Database_TestCase
{
public function testComplexQuery()
{
$queryTable = $this->getConnection()->createQueryTable(
'myComplexQuery', 'SELECT ...複雑なクエリ...'
);
$expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml")
->getTable("myComplexQuery");
$this->assertTablesEqual($expectedTable, $queryTable);
}
}
もちろん、複数のテーブルの状態を一度に確かめたり クエリデータセットをファイルベースのデータセットと比較したりすることも可能です。 データセットのアサーションには二通りの方法があります。
接続の Database (DB) データセットを使い、 それをファイルベースのデータセットと比較する。
class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
{
public function testCreateDataSetAssertion()
{
$dataSet = $this->getConnection()->createDataSet(array('guestbook'));
$expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
$this->assertDataSetsEqual($expectedDataSet, $dataSet);
}
}
データセットを自分で作ることもできます。
class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
{
public function testManualDataSetAssertion()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_QueryDataSet();
$dataSet->addTable('guestbook', 'SELECT id, content, user FROM guestbook'); // 追加のテーブル
$expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
$this->assertDataSetsEqual($expectedDataSet, $dataSet);
}
}
いいえ。PHPUnit は、テストスイートの開始時にすべてのデータベースオブジェクトが存在することを前提とします。 データベースやテーブル、シーケンス、トリガー、そしてビューなどは、 テストスイートを実行する前に作っておく必要があります。
Doctrine 2 や eZ Components の強力なツールを使えば、定義済みのデータ構造からデータベーススキーマを作成できます。 しかし、これらを使うには PHPUnit extension にフックで組み込まねばなりません。 そうしないと、テストスイートを実行する前にデータベースの自動再作成ができなくなります。
各テストの実行後はデータベースをクリアするので、 テストを実行するたびにデータベースを再作成する必要はありません。 事前に作ったデータベースをずっと使いまわすことができます。
いいえ。PDO が必要なのは、フィクスチャの準備や後始末とアサーションのときだけです。 テスト対象のコード内では、なんでもお好みの方法でデータベースにアクセスできます。
テストケースの getConnection() メソッドで作った
PDO インスタンスをキャッシュしていなければ、
データベースを使うテストを実行するたびにデータベースへの接続の数は増加し続けます。
デフォルトの設定では MySQL が受け付ける同時接続は 100 までであり、
他のデータベースにも同様の接続数制限があります。
「自前でのデータベーステストケースの抽象化」 に、このエラーを回避する方法を示しています。 ひとつの PDO インスタンスをキャッシュして、すべてのテストで使いまわす方法です。
新しいテストケースクラスを作成する際には、 これから書くべきテストの内容をはっきりさせるために、 まず最初は以下のような空のテストメソッドを書きたくなることでしょう。
public function testSomething()
{
}
しかし、PHPUnit フレームワークでは空のメソッドを「成功した」
と判断してしまうという問題があります。このような解釈ミスがあると、
テスト結果のレポートが無意味になってしまいます。
そのテストがほんとうに成功したのか、
それともまだテストが実装されていないのかが判断できないからです。
実装していないテストメソッドの中で $this->fail()
をコールするようにしたところで事態は何も変わりません。
こうすると、テストが「失敗した」と判断されてしまいます。
これは未実装のテストが「成功」と判断されてしまうのと同じくらいまずいことです
(訳注: レポートを見ても、そのテストがほんとうに失敗したのか、
まだ実装されていないだけなのかがわかりません)。
テストの成功を青信号、失敗を赤信号と考えるなら、
テストが未完成あるいは未実装であることを表すための黄信号が必要です。
そのような場合に使用するインターフェイスが
PHPUnit_Framework_IncompleteTest で、
これは未完成あるいは未実装のテストメソッドで発生する例外を表すものです。
このインターフェイスの標準的な実装が
PHPUnit_Framework_IncompleteTestError です。
例 9.1
では SampleTest というテストケースクラスを定義しています。
便利なメソッド markTestIncomplete()
(これは、自動的に PHPUnit_Framework_IncompleteTestError
を発生させます) をテストメソッド内でコールすることで、
このメソッドがまだ完成していないことをはっきりさせます。
例 9.1: テストに未完成の印をつける
<?php
class SampleTest extends PHPUnit_Framework_TestCase
{
public function testSomething()
{
// オプション: お望みなら、ここで何かのテストをしてください。
$this->assertTrue(TRUE, 'これは動いているはずです。');
// ここで処理を止め、テストが未完成であるという印をつけます。
$this->markTestIncomplete(
'このテストは、まだ実装されていません。'
);
}
}
?>
未完成のテストは、PHPUnit のコマンドライン版テストランナーでは以下のように
I で表されます。
phpunit --verbose SampleTest
PHPUnit 3.7.0 by Sebastian Bergmann.
I
Time: 0 seconds, Memory: 3.75Mb
There was 1 incomplete test:
1) SampleTest::testSomething
このテストは、まだ実装されていません。
/home/sb/SampleTest.php:12
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 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
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.7.0 by Sebastian Bergmann.
S
Time: 0 seconds, Memory: 3.75Mb
There was 1 skipped test:
1) DatabaseTest::testConnection
MySQLi 拡張モジュールが使用できません。
/home/sb/DatabaseTest.php:9
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Skipped: 1.表 9.2 に、テストを省略するための API を示します。
表9.2 テストを省略するための API
| メソッド | 意味 |
|---|---|
void markTestSkipped() | 現在のテストを省略扱いにします。 |
void markTestSkipped(string $message) | 現在のテストを省略扱いにします。それを説明する文字列として $message を使用します。 |
ここまでに示したメソッドに加えて、
@requires アノテーションを使って共通の事前条件を記述することもできます。
表9.3 @requires の例用例
| 型 | 取り得る値 | 例 | 別の例 |
|---|---|---|---|
PHP | PHP のバージョン | @requires PHP 5.3.3 | @requires PHP 5.4-dev |
PHPUnit | PHPUnit のバージョン | @requires PHPUnit 3.6.3 | @requires PHPUnit 3.7 |
function | function_exists に渡せるパラメータ | @requires function imap_open | @requires function ReflectionMethod::setAccessible |
extension | 拡張モジュール名 | @requires extension mysqli | @requires extension curl |
例 9.3: @requires を使ったテストケースのスキップ
<?php
/**
* @requires extension mysqli
*/
class DatabaseTest extends PHPUnit_Framework_TestCase
{
/**
* @requires PHP 5.3
*/
public function testConnection()
{
// このテストには mysqli 拡張モジュールと PHP 5.3 以降が必須です
}
// ... その他のすべてのテストには mysqli 拡張モジュールが必須です
}
?>
特定のバージョンの PHP でしか使えない構文を利用する場合は、 「テストスイート」 にあるように XML 設定ファイルでのバージョン依存のインクルードを検討しましょう。
Gerard Meszaros は、テストダブルの概念を [Meszaros2007] でこのように述べています。
PHPUnit の getMock($className) メソッドを使うと、
指定した元クラスのテストダブルとして振る舞うオブジェクトを自動的に生成することができます。
このテストダブルオブジェクトは、元クラスのオブジェクトを要するすべての場面で使うことができます。
デフォルトでは、元クラスのすべてのメソッドが置き換えられて、
(元のメソッドは呼び出さずに) 単に NULL
を返すだけのダミー実装になります。たとえば
will($this->returnValue()) メソッドを使うと、
ダミー実装がコールされたときに値を返すよう設定することができます。
final, private および
static メソッドのスタブやモックは作れないことに注意しましょう。
PHPUnit のテストダブル機能ではこれらを無視し、元のメソッドの振る舞いをそのまま維持します。
パラメータの管理方法が変わったことに気をつけましょう。 以前の実装ではオブジェクトのすべてのパラメータをクローンしており、 あるメソッドに渡されたオブジェクトが同じものであるかどうかを確かめることができませんでした。 例 10.14 に、新しい実装の活用例を示します。 例 10.15 に、以前の挙動に戻す方法を示します。
実際のオブジェクトを置き換えて、 設定した何らかの値を (オプションで) 返すようなテストダブルのことを スタブ といいます。 スタブ を使うと、 「SUT が依存している実際のコンポーネントを置き換え、 SUT の入力を間接的にコントロールできるようにすることができます。 これにより、SUT が他の何者も実行しないことを強制させることができます。」
例 10.2
に、スタブメソッドの作成と返り値の設定の方法を示します。まず、
PHPUnit_Framework_TestCase クラスの
getMock() メソッドを用いて
SomeClass オブジェクトのスタブを作成します
(例 10.1)。
次に、PHPUnit が提供する、いわゆる
Fluent Interface
(流れるようなインターフェイス)
を用いてスタブの振る舞いを指定します。簡単に言うと、
いくつもの一時オブジェクトを作成して、
それらを連結するといった操作は必要ないということです。
そのかわりに、例にあるようにメソッドの呼び出しを連結します。
このほうが、より読みやすく "流れるような" コードとなります。
例 10.2: メソッドに固定値を返させるスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// $stub->doSomething() をコールすると
// 'foo' を返すようになります
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
舞台裏では、getMock() メソッドが使われたときに
PHPUnit が自動的に、求める振る舞いを実装した新たな PHP のクラスを生成しています。
生成されるテストダブルクラスの設定は、
getMock() メソッドのオプションの引数を使って行います。
デフォルトでは、指定したクラスのすべてのメソッドが単に NULL を返すだけのテストダブルとなります。返り値を変更するには、たとえば will($this->returnValue()) を使います。
オプションの第二パラメータを指定すると、その配列の中に含まれる名前のメソッドだけがテストダブルに置き換えらて、その他のメソッドはそのままとなります。パラメータに NULL を渡すと、どのメソッドも置き換えません。
オプションの第三パラメータには、元クラスのコンストラクタに渡すパラメータの配列を渡します (デフォルトでは、コンストラクタはダミー実装に置き換えられません)。
オプションの第四パラメータを使うと、生成されるテストダブルクラスのクラス名を指定することができます。
オプションの第五パラメータを使うと、元クラスのコンストラクタを呼び出さないようにすることができます。
オプションの第六パラメータを使うと、元クラスの clone コンストラクタを呼び出さないようにすることができます。
オプションの第七パラメータを使うと、テストダブルクラスの生成時に __autoload() を無効にすることができます。
もうひとつのやり方として、生成されたテストダブルクラスの設定を モックビルダー API で行うことができます。 例 10.3 に例を示します。 モックビルダーで使えるメソッドの一覧は次のとおりです。
setMethods(array $methods) をモックビルダーオブジェクト上でコールすると、テストダブルで置き換えるメソッドを指定することができます。その他のメソッドの挙動は変更しません。setMethods(NULL) とすると、どのメソッドも置き換えません。
setConstructorArgs(array $args) をコールしてパラメータの配列を渡すと、それを元クラスのコンストラクタに渡すことができます (デフォルトのダミー実装では、コンストラクタは置き換えません)。
setMockClassName($name) を使うと、生成されるテストダブルクラスのクラス名を指定することができます。
disableOriginalConstructor() を使うと、元クラスのコンストラクタを無効にすることができます。
disableOriginalClone() を使うと、元クラスのクローンコンストラクタを無効にすることができます。
disableAutoload() を使うと、テストダブルクラスを生成するときに __autoload() を無効にすることができます。
例 10.3: モックビルダー API を使った、生成されるテストダブルクラスの変更
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMockBuilder('SomeClass')
->disableOriginalConstructor()
->getMock();
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// $stub->doSomething() をコールすると
// 'foo' を返すようになります
$this->assertEquals('foo', $stub->doSomething());
}
}
?>
時には、メソッドをコールした際の引数のひとつを
(そのまま) スタブメソッドコールの返り値としたいこともあるでしょう。
例 10.4 は、
returnValue() のかわりに
returnArgument() を用いてこれを実現する例です。
例 10.4: メソッドに引数のひとつを返させるスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnArgumentStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->returnArgument(0));
// $stub->doSomething('foo') は 'foo' を返します
$this->assertEquals('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') は 'bar' を返します
$this->assertEquals('bar', $stub->doSomething('bar'));
}
}
?>
流れるようなインターフェイスをテストするときには、
スタブメソッドがオブジェクト自身への参照を返すようにできると便利です。
例 10.5 は、
returnSelf() を使ってこれを実現する例です。
例 10.5: スタブオブジェクトへの参照を返すメソッドのスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnSelf()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->returnSelf());
// $stub->doSomething() は $stub を返します
$this->assertSame($stub, $stub->doSomething());
}
}
?>
スタブメソッドをコールした結果として、
定義済みの引数リストにあわせて異なる値を返さなければならないこともあるでしょう。
returnValueMap() を使えば、
マップを作って引数と関連付け、それを返り値に対応させることができます。
例 10.6 を参照ください。
例 10.6: メソッドにマップからの値を返させるスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnValueMapStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// 値を返すための、引数のマップを作製します
$map = array(
array('a', 'b', 'c', 'd'),
array('e', 'f', 'g', 'h')
);
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValueMap($map));
// $stub->doSomething() は、渡した引数に応じて異なる値を返します
$this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
$this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
}
}
?>
スタブメソッドをコールした結果として固定値
(returnValue() を参照ください) や (不変の) 引数
(returnArgument() を参照ください)
ではなく計算した値を返したい場合は、
returnCallback() を使用します。
これは、スタブメソッドからコールバック関数やメソッドの結果を返させます。
例 10.7
を参照ください。
例 10.7: メソッドにコールバックからの値を返させるスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testReturnCallbackStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->returnCallback('str_rot13'));
// $stub->doSomething($argument) は str_rot13($argument) を返します
$this->assertEquals('fbzrguvat', $stub->doSomething('something'));
}
}
?>
コールバックメソッドを設定するよりももう少しシンプルな方法として、
希望する返り値のリストを指定することもできます。この場合に使うのは
onConsecutiveCalls() メソッドです。
例 10.8
の例を参照ください。
例 10.8: メソッドに、リストで指定した値をその順で返させるスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testOnConsecutiveCallsStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->onConsecutiveCalls(2, 3, 5, 7));
// $stub->doSomething() は毎回異なる値を返します
$this->assertEquals(2, $stub->doSomething());
$this->assertEquals(3, $stub->doSomething());
$this->assertEquals(5, $stub->doSomething());
}
}
?>
値を返すのではなく、スタブメソッドで例外を発生させることもできます。
例 10.9
に、throwException() でこれを行う方法を示します。
例 10.9: メソッドに例外をスローさせるスタブ
<?php
require_once 'SomeClass.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testThrowExceptionStub()
{
// SomeClass クラスのスタブを作成します
$stub = $this->getMock('SomeClass');
// スタブの設定を行います
$stub->expects($this->any())
->method('doSomething')
->will($this->throwException(new Exception));
// $stub->doSomething() は例外をスローします
$stub->doSomething();
}
}
?>
また、スタブを使用することで、よりよい設計を行うことができるようにもなります。
あちこちで使用されているリソースを単一の窓口 (façade : ファサード)
経由でアクセスするようにすることで、
それを簡単にスタブに置き換えられるようになります。例えば、
データベースへのアクセスのコードをそこらじゅうにちりばめるのではなく、
その代わりに IDatabase インターフェイスを実装した単一の
Database オブジェクトを使用するようにします。すると、
IDatabase を実装したスタブを作成することで、
それをテストに使用できるようになるのです。同時に、
テストを行う際にスタブデータベースを使用するか
本物のデータベースを使用するかを選択できるようになります。
つまり開発時にはローカル環境でテストし、
統合テスト時には実際のデータベースでテストするといったことができるようになるのです。
スタブ化しなければならない機能は、たいてい同一オブジェクト内で密結合しています。 この機能ををひとつの結合したインターフェイスにまとめることで、 システムのそれ以外の部分との結合を緩やかにすることができます。
実際のオブジェクトを置き換えて、 (メソッドがコールされたことなどの) 期待する内容を検証するテストダブルのことを モック といいます。
モックオブジェクト は SUT の間接的な出力の内容を検証するために使用する観測地点です。 一般的に、モックオブジェクトにはテスト用スタブの機能も含まれます。 まだテストに失敗していない場合に、間接的な出力の検証用の値を SUT に返す機能です。 したがって、モックオブジェクトとは テスト用スタブにアサーション機能を足しただけのものとは異なります。 それ以外の用途にも使うことができます。
ひとつ例を示します。ここでは、別のオブジェクトを観察している
あるオブジェクトの特定のメソッド (この例では update())
が正しくコールされたかどうかを調べるものとします。
例 10.10
は、テスト対象のシステム (SUT) の一部である
Subject クラスと Observer クラスのコードです。
例 10.10: テスト対象のシステム (SUT) の一部である Subject クラスと Observer クラス
<?php
class Subject
{
protected $observers = array();
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// なにかをします
// ...
// なにかしたということをオブザーバに通知します
$this->notify('something');
}
public function doSomethingBad()
{
foreach ($this->observers as $observer) {
$observer->reportError(42, 'Something bad happened', $this);
}
}
protected function notify($argument)
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
// その他のメソッド
}
class Observer
{
public function update($argument)
{
// なにかをします
}
public function reportError($errorCode, $errorMessage, Subject $subject)
{
// なにかをします
}
// その他のメソッド
}
?>
例 10.11
では、モックオブジェクトを作成して
Subject オブジェクトと Observer
オブジェクトの対話をテストする方法を説明します。
まず
PHPUnit_Framework_TestCase クラスの
getMock() メソッド
を使用して Observer のモックオブジェクトを作成します。
getMock() メソッドの二番目の (オプションの)
パラメータに配列を指定しているので、Observer
クラスの中の update() メソッドについてのみモック実装が作成されます。
例 10.11: あるメソッドが、指定した引数で一度だけコールされることを確かめるテスト
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testObserversAreUpdated()
{
// 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();
}
}
?>
with() メソッドには任意の数の引数を渡すことができます。
これは、モック対象のメソッドのパラメータ数に対応します。
メソッドの引数に対して、単なるマッチだけでなくより高度な制約を指定することもできます。
例 10.12: メソッドが引数つきでコールされることを、さまざまな制約の下でテストする例
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testErrorReported()
{
// Observer クラスのモックを作成します。
// reportError() メソッドをモックします。
$observer = $this->getMock('Observer', array('reportError'));
$observer->expects($this->once())
->method('reportError')
->with($this->greaterThan(0),
$this->stringContains('Something'),
$this->anything());
$subject = new Subject;
$subject->attach($observer);
// doSomethingBad() メソッドは、
// reportError() メソッドを通じてオブザーバにエラーを報告しなければなりません。
$subject->doSomethingBad();
}
}
?>
表 4.3 はメソッドの引数に適用できる制約、そして 表 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 回目に実行された際にマッチするオブジェクトを返します。 |
getMockForAbstractClass() メソッドは、
抽象クラスのモックオブジェクトを返します。
そのクラスのすべての抽象メソッドがモックの対象となります。
これを使えば、抽象クラスにある具象メソッドをテストすることができます。
例 10.13: 抽象クラスの具象メソッドのテスト
<?php
abstract class AbstractClass
{
public function concreteMethod()
{
return $this->abstractMethod();
}
public abstract function abstractMethod();
}
class AbstractClassTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod()
{
$stub = $this->getMockForAbstractClass('AbstractClass');
$stub->expects($this->any())
->method('abstractMethod')
->will($this->returnValue(TRUE));
$this->assertTrue($stub->concreteMethod());
}
}
?>
例 10.14: あるメソッドが一度呼ばれ、同じオブジェクトが渡されたことを調べるテスト
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testIdenticalObjectPassed()
{
$expectedObject = new stdClass;
$mock = $this->getMock('stdClass', array('foo'));
$mock->expects($this->once())
->method('foo')
->with($this->identicalTo($expectedObject));
$mock->foo($expectedObject);
}
}
?>
例 10.15: パラメータのクローンを有効にしたモックオブジェクトの作成
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testIdenticalObjectPassed()
{
$cloneArguments = true;
$mock = $this->getMock(
'stdClass',
array(),
array(),
'',
FALSE,
TRUE,
TRUE,
$cloneArguments
);
// あるいはモックビルダーを使います
$mock = $this->getMockBuilder('stdClass')->enableArgumentCloning()->getMock();
// このモックはパラメータをクローンし、identicalTo 制約を満たさないようになります。
}
}
?>
ウェブサービスとのやりとりを行うアプリケーションを、
実際にウェブサービスとやりとりすることなくテストしたくなることもあるでしょう。
ウェブサービスのスタブやモックを作りやすくするために getMockFromWsdl()
メソッドが用意されており、これは getMock() (上を参照ください)
とほぼ同様に使うことができます。唯一の違いは、
getMockFromWsdl() が返すスタブやモックが WSDL
のウェブサービス記述にもとづくものであるのに対して getMock()
が返すスタブやモックが PHP のクラスやインターフェイスにもとづくものであるという点です。
例 10.16
は、getMockFromWsdl() を使って
GoogleSearch.wsdl に記述されたウェブサービスのスタブを作る例です。
例 10.16: ウェブサービスのスタブ
<?php
class GoogleTest extends PHPUnit_Framework_TestCase
{
public function testSearch()
{
$googleSearch = $this->getMockFromWsdl(
'GoogleSearch.wsdl', 'GoogleSearch'
);
$directoryCategory = new StdClass;
$directoryCategory->fullViewableName = '';
$directoryCategory->specialEncoding = '';
$element = new StdClass;
$element->summary = '';
$element->URL = 'http://www.phpunit.de/';
$element->snippet = '...';
$element->title = '<b>PHPUnit</b>';
$element->cachedSize = '11k';
$element->relatedInformationPresent = TRUE;
$element->hostName = 'www.phpunit.de';
$element->directoryCategory = $directoryCategory;
$element->directoryTitle = '';
$result = new StdClass;
$result->documentFiltering = FALSE;
$result->searchComments = '';
$result->estimatedTotalResultsCount = 378000;
$result->estimateIsExact = FALSE;
$result->resultElements = array($element);
$result->searchQuery = 'PHPUnit';
$result->startIndex = 1;
$result->endIndex = 1;
$result->searchTips = '';
$result->directoryCategories = array();
$result->searchTime = 0.248822;
$googleSearch->expects($this->any())
->method('doGoogleSearch')
->will($this->returnValue($result));
/**
* $googleSearch->doGoogleSearch() はスタブが用意した結果を返し、
* ウェブサービスの doGoogleSearch() が呼び出されることはありません
*/
$this->assertEquals(
$result,
$googleSearch->doGoogleSearch(
'00000000000000000000000000000000',
'PHPUnit',
0,
1,
FALSE,
'',
FALSE,
'',
'',
''
)
);
}
}
?>
vfsStream は 仮想ファイルシステム 用の ストリームラッパー で、 ユニットテストにおいて実際のファイルシステムのモックを作るときに有用です。
vfsStream をインストールするには、配布元の PEAR チャンネル
(pear.bovigo.org)
をローカルの PEAR 環境に登録しなければなりません。
pear channel-discover pear.bovigo.orgこれが必要なのは最初の一度だけです。これで、 PEAR インストーラを使って vfsStream をインストールできるようになりました。
pear install bovigo/vfsStream-beta例 10.17 は、ファイルシステムを操作するクラスの例です。
例 10.17: ファイルシステムを操作するクラス
<?php
class Example
{
protected $id;
protected $directory;
public function __construct($id)
{
$this->id = $id;
}
public function setDirectory($directory)
{
$this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;
if (!file_exists($this->directory)) {
mkdir($this->directory, 0700, TRUE);
}
}
}?>
vfsStream のような仮想ファイルシステムがなければ、外部への影響なしに
setDirectory() メソッドを個別にテストすることができません
(例 10.18
を参照ください)。
例 10.18: ファイルシステムを操作するクラスのテスト
<?php
require_once 'Example.php';
class ExampleTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
public function testDirectoryIsCreated()
{
$example = new Example('id');
$this->assertFalse(file_exists(dirname(__FILE__) . '/id'));
$example->setDirectory(dirname(__FILE__));
$this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
}
protected function tearDown()
{
if (file_exists(dirname(__FILE__) . '/id')) {
rmdir(dirname(__FILE__) . '/id');
}
}
}
?>
この方式には、次のような問題があります。
外部のリソースを使うため、ファイルシステムのテストが断続的になる可能性があります。その結果、テストがあまり当てにならないものになります。
setUp() と tearDown() で、テストの前後にそのディレクトリがないことを確認する必要があります。
tearDown() メソッドを実行する前にテストが異常終了したときに、ファイルシステム上にディレクトリが残ったままとなります。
例 10.19 は、vfsStream を使ってファイルシステムのモックを作成し、 ファイルシステムを操作するクラスのテストを行う例です。
例 10.19: ファイルシステムを操作するクラスのテストにおけるファイルシステムのモックの作成
<?php
require_once 'vfsStream/vfsStream.php';
require_once 'Example.php';
class ExampleTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
vfsStreamWrapper::register();
vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
}
public function testDirectoryIsCreated()
{
$example = new Example('id');
$this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));
$example->setDirectory(vfsStream::url('exampleDir'));
$this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
}
}
?>
この方式には次のような利点があります。
テストが簡潔になります。
vfsStream が、テスト対象のコードから操作するファイルシステム環境を用意してくれるので、開発者はそれを自由に扱えるようになります。
実際のファイルシステムを操作することがなくなるので、tearDown() メソッドでの後始末が不要になります。
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. ユニットテストには、こんなに多くの利点がある。
まとめよう。ユニットテストをうまく組み込めば、 プログラムを変更する際の手間やリスクをより減らすことになるのだ。 プロジェクトが【中略】のアーキテクチャに関する大きな改修【中略】を素早く、自信を持って行うことを可能にするだろう。 | ||
| --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 'BankAccount.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
protected $ba;
protected function setUp()
{
$this->ba = new BankAccount;
}
public function testBalanceIsInitiallyZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
public function testBalanceCannotBecomeNegative()
{
try {
$this->ba->withdrawMoney(1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
public function testBalanceCannotBecomeNegative2()
{
try {
$this->ba->depositMoney(-1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
}
?>
それでは、最初のテスト testBalanceIsInitiallyZero()
をクリアするために必要な最小限のコードを書いていきましょう。必要なのは、
BankAccount クラスの getBalance()
メソッドを
例 12.2
のように実装することです。
例 12.2: テスト testBalanceIsInitiallyZero() をクリアするために必要なコード
<?php
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
}
?>
これで最初のテストはクリアすることになりましたが、 2 番目のテストには失敗します。なぜなら、 テストメソッド内でコールしているメソッドがまだ実装されていないからです。
phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.
.
Fatal error: Call to undefined method BankAccount::withdrawMoney()
ふたつめの規約のテストをクリアするには、withdrawMoney()、
depositMoney() および setBalance()
の各メソッドを
例 12.3
のように実装しなければなりません。これらのメソッドは、
規約に反するような引数でコールされた場合には BankAccountException
を発生させるように実装しています。
例 12.3: 完全な BankAccount クラス
<?php
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
if ($balance >= 0) {
$this->balance = $balance;
} else {
throw new BankAccountException;
}
}
public function depositMoney($balance)
{
$this->setBalance($this->getBalance() + $balance);
return $this->getBalance();
}
public function withdrawMoney($balance)
{
$this->setBalance($this->getBalance() - $balance);
return $this->getBalance();
}
}
?>
これで、2 つめの規約に関するテストにもクリアするようになります。
phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
別の方法としては、PHPUnit_Framework_Assert
クラスが提供する静的なアサーションメソッドを用いて、コード内に
「規約による設計」方式のアサーションを記述するというものもあります。
例 12.4
がその例です。これらのアサーションのいずれかに失敗すると、例外
PHPUnit_Framework_AssertionFailedError が発生します。
この方式を用いると、条件チェックのコードを減らすことができてテストが読みやすくなります。
ただ、プログラムの実行時にも PHPUnit が必要になってしまいます。
例 12.4: 「規約による設計」のアサーションを使用した BankAccount クラス
<?php
class BankAccount
{
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
PHPUnit_Framework_Assert::assertTrue($balance >= 0);
$this->balance = $balance;
}
public function depositMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
$this->setBalance($this->getBalance() + $amount);
return $this->getBalance();
}
public function withdrawMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
PHPUnit_Framework_Assert::assertTrue($this->balance >= $amount);
$this->setBalance($this->getBalance() - $amount);
return $this->getBalance();
}
}
?>
規約を満たすための条件をテスト内に記述することで、「規約による設計」
方式で BankAccount クラスをプログラミングしてきました。
次に、テストファーストプログラミングの考え方にしたがって、
テストをクリアするために必要なコードを記述してきました。
でも、ひとつ忘れてしまったことがあります。それは、
setBalance()、depositMoney()
および withdrawMoney() に正当な値を指定した場合に、
正常に動作することを確かめるテストを書くことです。
自分が書いたテストが妥当なものなのか、
それで十分なのかを調べるためのテストが必要ですね。次の章では、そのための
「コードカバレッジ解析」について説明します。
[Astels2006] において、Dave Astels は次のように述べています。
エクストリーム・プログラミング (日本語) は本来、壊れる可能性のあるものはすべてテストするという決まりがあった。
今ではしかし、エクストリーム・プログラミングにおけるテスト手法は テスト駆動開発 (日本語) に進化した (第 12 章 を参照ください)。
しかし、各種ツールは未だにテストの語彙で考えることを強要し、 スペックではなくアサーションで考えさせようとする。
So if it's not about testing, what's it about? (テストじゃないっていうけど、じゃあいったい何なの?) It's about figuring out what you are trying to do before you run off half-cocked to try to do it. You write a specification that nails down a small aspect of behaviour in a concise, unambiguous, and executable form. It's that simple. Does that mean you write tests? No. It means you write specifications of what your code will have to do. It means you specify the behaviour of your code ahead of time. But not far ahead of time. In fact, just before you write the code is best because that's when you have as much information at hand as you will up to that point. Like well done TDD, you work in tiny increments... specifying one small aspect of behaviour at a time, then implementing it. - あなたがこれから何をしようとしているのかを事前にきちんと把握することで、 準備不足のまま逃げ出してしまうようなはめにならないようにするものです。 あなたが書くスペックは、ある振る舞いのちょっとした側面を 簡潔で明確かつ実行可能な形式で表したものとなります。 ただそれだけの簡単なこと。 え?それってテストじゃないのかって? そう、テストではないのです。 あなたは「そのコードがどう動くべきか」という仕様 (スペック) を書くのです。実際のコードを書く前にコードの振る舞いを定義することになります。 とはいえ、それはコードを書くずっと前にということではありません。 実際のところは、コードを書く直前にスペックを書くのがよいでしょう。 コードを書く際に利用する情報とスペックを書く際に利用する情報がほぼ同じになるからです。 TDD のときと同様、小さい作業の積み重ねで進めていきます。 一度に定義する振る舞いは小さなものにとどめ、 その単位で実装を進めていくのです。 When you realize that it's all about specifying behaviour and not writing tests, your point of view shifts. Suddenly the idea of having a Test class for each of your production classes is ridiculously limiting. And the thought of testing each of your methods with its own test method (in a 1-1 relationship) will be laughable. - 仕様を定義することとテストをかくことの違いを理解すれば、ものの見方が変わります。 実装クラスのひとつひとつに対応するテストクラスを作成するなどという考え方が おそろしく窮屈なものに見えてくることでしょう。 個々のメソッドにそれぞれテストメソッドを (1 対 1 対応で) 用意するなんてばかばかしくなってきます。 | ||
| --Dave Astels | ||
振舞駆動開発 (日本語) が注目するのは、ソフトウェア開発の際に使用する言語やインタラクションです。 振舞駆動開発では、よく目にする ドメイン駆動設計 の語彙を用いてコードの目的や利点を記述します。 これにより、開発者が技術的な詳細よりも「なぜそのコードを書かなければいけないのか」 に注目できるようになります。そして、 コードを書くときに使う言語とドメインエキスパートが話す用語との間の翻訳の手間を最小限にできます。
PHPUnit_Extensions_Story_TestCase
クラスはストーリーフレームワークを提供します。
これは、振舞駆動開発のための
ドメイン特化言語
(日本語)
の定義を支援するものです。次のようにインストールします。
pear install phpunit/PHPUnit_Story
シナリオ (scenario) の中において、
given() や when() そして
then() が ステップ (step)
を表します。
and() は直前のステップと同じ種類のものを表します。
次のメソッドが
PHPUnit_Extensions_Story_TestCase
で abstract として宣言されており、
これらを実装する必要があります。
runGiven(&$world, $action, $arguments)
...
runWhen(&$world, $action, $arguments)
...
runThen(&$world, $action, $arguments)
...
この節では、ボウリングゲームのスコアを計算するクラスの例を見てみましょう。 ボウリングのルールは次のとおりです。
ひとつのゲームは 10 フレームで構成される
10 本のピンを倒すため、各フレームでプレイヤーは 2 回投げることができる
各フレームのスコアは倒したピンの総数で、ストライクやスペアの際にはさらにボーナスが追加される
スペアとは、2 回投げて 10 本のピンをすべて倒すこと
その場合のボーナスは、次に投げたときに倒したピンの数
ストライクとは、1 投目で 10 本のピンをすべて倒すこと
その場合のボーナスは、次の 2 投で倒したピンの数
例 13.1
は、上にまとめたルールを
PHPUnit_Extensions_Story_TestCase
でスペックシナリオとして書き下ろしたものです。
例 13.1: BowlingGame クラスのスペック
<?php
require_once 'PHPUnit/Extensions/Story/TestCase.php';
require_once 'BowlingGame.php';
class BowlingGameSpec extends PHPUnit_Extensions_Story_TestCase
{
/**
* @scenario
*/
public function scoreForGutterGameIs0()
{
$this->given('New game')
->then('Score should be', 0);
}
/**
* @scenario
*/
public function scoreForAllOnesIs20()
{
$this->given('New game')
->when('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->and('Player rolls', 1)
->then('Score should be', 20);
}
/**
* @scenario
*/
public function scoreForOneSpareAnd3Is16()
{
$this->given('New game')
->when('Player rolls', 5)
->and('Player rolls', 5)
->and('Player rolls', 3)
->then('Score should be', 16);
}
/**
* @scenario
*/
public function scoreForOneStrikeAnd3And4Is24()
{
$this->given('New game')
->when('Player rolls', 10)
->and('Player rolls', 3)
->and('Player rolls', 4)
->then('Score should be', 24);
}
/**
* @scenario
*/
public function scoreForPerfectGameIs300()
{
$this->given('New game')
->when('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->and('Player rolls', 10)
->then('Score should be', 300);
}
public function runGiven(&$world, $action, $arguments)
{
switch($action) {
case 'New game': {
$world['game'] = new BowlingGame;
$world['rolls'] = 0;
}
break;
default: {
return $this->notImplemented($action);
}
}
}
public function runWhen(&$world, $action, $arguments)
{
switch($action) {
case 'Player rolls': {
$world['game']->roll($arguments[0]);
$world['rolls']++;
}
break;
default: {
return $this->notImplemented($action);
}
}
}
public function runThen(&$world, $action, $arguments)
{
switch($action) {
case 'Score should be': {
for ($i = $world['rolls']; $i < 20; $i++) {
$world['game']->roll(0);
}
$this->assertEquals($arguments[0], $world['game']->score());
}
break;
default: {
return $this->notImplemented($action);
}
}
}
}
?>
phpunit --printer PHPUnit_Extensions_Story_ResultPrinter_Text BowlingGameSpec
PHPUnit 3.7.0 by Sebastian Bergmann.
BowlingGameSpec
[x] Score for gutter game is 0
Given New game
Then Score should be 0
[x] Score for all ones is 20
Given New game
When Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
and Player rolls 1
Then Score should be 20
[x] Score for one spare and 3 is 16
Given New game
When Player rolls 5
and Player rolls 5
and Player rolls 3
Then Score should be 16
[x] Score for one strike and 3 and 4 is 24
Given New game
When Player rolls 10
and Player rolls 3
and Player rolls 4
Then Score should be 24
[x] Score for perfect game is 300
Given New game
When Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
and Player rolls 10
Then Score should be 300
Scenarios: 5, Failed: 0, Skipped: 0, Incomplete: 0.The beauty of testing is found not in the effort but in the effiency. - テストの美学は、どれだけ汗を流したかではなく どれだけ効率的であるかである。 Knowing what should be tested is beautiful, and knowing what is being tested is beautiful. - 何をテストすべきなのかを知ること、何がテストされているのかを知ること。 これが大切だ。 | ||
| --Murali Nandigama | ||
この章では、PHPUnit のコードカバレッジ機能について学びます。 これは、テストを実行したときに、実装コードのどの部分が実行されたかを調べるものです。 次のような疑問に対する答えとなるでしょう。
テストされていないコードを見つけるには? 言い換えれば、まだテストで カバーされていない部分を見つけるには?
完全にテストができたことをどうやって確認するの?
ステートメントカバレッジというのは、たとえば 100 行のコードで構成されるメソッドがあった場合に、 もしテストで実際に実行されたのがそのうちの 75 行だけだったなら、 そのメソッドのコードカバレッジは 75 パーセントだと考えるということです。
PHPUnit のコードカバレッジ解析では PHP_CodeCoverage コンポーネントを使っています。このコンポーネントは、 Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。
それでは、例 12.3
の BankAccount クラスについての
コードカバレッジレポートを作成してみましょう。
phpunit --coverage-html ./report BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
Generating report, this may take a moment.図 14.1 は、コードカバレッジレポートの一部を抜粋したものです。 テスト時に実行された行は、緑色で強調表示されます。 実行可能なコードであるにもかかわらず実行されなかった行については赤色で強調表示されます。 また、"無意味なコード" についてはグレーで強調表示されます。 行の左にある数字は、その行をカバーするテストの数を表します。
BankAccount のコードカバレッジレポートからわかることは、
setBalance()、depositMoney()
をコールするテストがまだ存在しないということ、
そして withdrawMoney()
に正しい値を指定した場合のテストも存在しないということです。
BankAccountTest クラスに追加するテストを
例 14.1
に示します。これによって、BankAccount
クラスのテストケースを完全に網羅できるようになります。
例 14.1: 完全なコードカバレッジを達成するために欠けているテスト
<?php
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
// ...
public function testDepositWithdrawMoney()
{
$this->assertEquals(0, $this->ba->getBalance());
$this->ba->depositMoney(1);
$this->assertEquals(1, $this->ba->getBalance());
$this->ba->withdrawMoney(1);
$this->assertEquals(0, $this->ba->getBalance());
}
}
?>
図 14.2 は、
テストを追加した後の setBalance() のコードカバレッジです。
テストコードで @covers アノテーション
(表 B.1)
を参照ください) を使用すると、
そのテストメソッドがどのメソッドをテストしたいのかを指定することができます。
これを指定すると、指定したメソッドのコードカバレッジ情報のみを考慮します。
例 14.2
に例を示します。
例 14.2: どのメソッドを対象とするかを指定したテスト
<?php
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
protected $ba;
protected function setUp()
{
$this->ba = new BankAccount;
}
/**
* @covers BankAccount::getBalance
*/
public function testBalanceIsInitiallyZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
/**
* @covers BankAccount::withdrawMoney
*/
public function testBalanceCannotBecomeNegative()
{
try {
$this->ba->withdrawMoney(1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
/**
* @covers BankAccount::depositMoney
*/
public function testBalanceCannotBecomeNegative2()
{
try {
$this->ba->depositMoney(-1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
/**
* @covers BankAccount::getBalance
* @covers BankAccount::depositMoney
* @covers BankAccount::withdrawMoney
*/
public function testDepositWithdrawMoney()
{
$this->assertEquals(0, $this->ba->getBalance());
$this->ba->depositMoney(1);
$this->assertEquals(1, $this->ba->getBalance());
$this->ba->withdrawMoney(1);
$this->assertEquals(0, $this->ba->getBalance());
}
}
?>
あるテストが、一切メソッドをカバーしてはならないことも指定できます。
そのために使うのが @coversNothing アノテーションです。
(「@coversNothing」 を参照ください)。
これは、インテグレーションテストを書く際に
ユニットテストだけのコードカバレッジを生成させたい場合に便利です。
例 14.3: どのメソッドもカバーすべきでないことを指定したテスト
<?php
class GuestbookIntegrationTest extends PHPUnit_Extensions_Database_TestCase
{
/**
* @coversNothing
*/
public function testAddEntry()
{
$guestbook = new Guestbook();
$guestbook->addEntry("suzy", "Hello world!");
$queryTable = $this->getConnection()->createQueryTable(
'guestbook', 'SELECT * FROM guestbook'
);
$expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
->getTable("guestbook");
$this->assertTablesEqual($expectedTable, $queryTable);
}
}
?>
どうしてもテストができないコードブロックなどを、
コードカバレッジ解析時に無視させたいこともあるでしょう。
PHPUnit でこれを実現するには、
@codeCoverageIgnore、
@codeCoverageIgnoreStart および
@codeCoverageIgnoreEnd アノテーションを
例 14.4
のように使用します。
例 14.4: @codeCoverageIgnore、@codeCoverageIgnoreStart および @codeCoverageIgnoreEnd アノテーションの使用法
<?php
/**
* @codeCoverageIgnore
*/
class Foo
{
public function bar()
{
}
}
class Bar
{
/**
* @codeCoverageIgnore
*/
public function foo()
{
}
}
if (FALSE) {
// @codeCoverageIgnoreStart
print '*';
// @codeCoverageIgnoreEnd
}
?>
これらのアノテーションを使って無視するよう指定された行は、 (たとえ実行されなかったとしても) 実行されたものとみなされ、 強調表示されません。
デフォルトでは、1 行でもコードが実行されたソースコードファイルはすべて (そしてそのようなファイルのみが) レポートに含められます。 レポートに含まれるソースコードファイルは、 ホワイトリスト方式あるいはブラックリスト方式でフィルタリングすることができます。
ブラックリストには、PHPUnit 自身のソースコードファイルやテストファイルがデフォルトで登録されています。 ホワイトリストが空 (デフォルト) の場合はブラックリストを使用し、 ホワイトリストが空でない場合はホワイトリストを使用します。 ホワイトリスト内の各ファイルは、そのファイルが実行されるかどうかにかかわらず レポートに追加されます。追加されたファイルのすべての行は、 実行不能な行も含めて「実行されなかった行」とみなします。
PHPUnit の設定で processUncoveredFilesFromWhitelist="true"
(「コードカバレッジ対象のファイルの追加や除外」 を参照ください)
とすると、これらのファイルが PHP_CodeCoverage
に渡され、実行可能な行数を適切に算出します。
processUncoveredFilesFromWhitelist="true"
が設定されている場合のソースコードファイルの読み込みでは、
もしクラスや関数のスコープから外れるコードが含まれていたときに問題が起こる可能性があります。
PHPUnit の XML 設定ファイル (「コードカバレッジ対象のファイルの追加や除外」 を参照ください) を使って、ブラックリストやホワイトリストを制御することができます。 おすすめの方法は、ホワイトリスト方式を使ってコードカバレッジレポートに含めるファイルを制御することです。
ほとんどの場面では、PHPUnit が「行単位の」コードカバレッジ情報を提供してくれると考えて間違いないでしょう。 しかし、その情報収集の方法が原因で、特筆すべきエッジケースもいくつか存在します。
例 14.5:
<?php
// カバレッジは「行単位」であって文単位ではないので、
// 一行にまとめられた行はひとつのカバレッジ状態しか持ちません
if(false) this_function_call_shows_up_as_covered();
// コードカバレッジの内部動作上、これら 2 行は特別です。
// 次の行は「実行されていない」となります
if(false)
// 次の行は「実行されている」となります
// 実際のところ、ひとつ上の if 文のカバレッジ情報がここに表示されることになるからです!
will_also_show_up_as_coveraged();
// これを避けるには、必ず波括弧を使わなければなりません
if(false) {
this_call_will_never_show_up_as_covered();
}
?>
自動テストに慣れてくると、 ほかの目的のためにもテストを使いたくなってくることでしょう。 ここではそんな例を説明します。
一般的に、エクストリームプログラミングのようなアジャイルプロセスを採用しているプロジェクトでは、 ドキュメントの内容が実際の設計やコードに追いついていないことが多いものです。 エクストリームプログラミングでは コードの共同所有 (collective code ownership) を要求しており、 すべての開発者がシステム全体の動作を知っておく必要があります。 作成するテストに対して、そのクラスが何を行うべきなのかを示すような 「わかりやすい」名前をつけられるようにさえしておけば、PHPUnit の TestDox 機能を使用して自動的にドキュメントを生成することができます。 このドキュメントにより、開発者たちはプロジェクト内の各クラスが どのようにふるまうべきなのかを知ることができます。
PHPUnit の TestDox 機能は、テストクラス内のすべてのテストメソッドの名前を抽出し、
それを PHP 風のキャメルケースから通常の文に変換します。つまり
testBalanceIsInitiallyZero() が "Balance is initially zero"
のようになるわけです。最後のほうの数字のみが違うメソッド、例えば
testBalanceCannotBecomeNegative() と
testBalanceCannotBecomeNegative2() のようなものが存在した場合は、
文 "Balance cannot become negative" は一度のみ表示され、
全てのテストが成功したことを表します。
BankAccount クラスのアジャイルな文書
(例 12.1
を参照ください) を見てみましょう。
phpunit --testdox BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.
BankAccount
[x] Balance is initially zero
[x] Balance cannot become negative
また、アジャイルな文書を HTML あるいはプレーンテキスト形式で作成してファイルに書き出すこともできます。
この場合は、引数 --testdox-html
あるいは --testdox-text を使用します。
アジャイルな文書は、プロジェクト内であなたが作成しようとしている外部パッケージについて、 このように動作するであるという期待をまとめた文書にもなります。 外部のパッケージを使用するときには、 そのパッケージが期待通りに動作しなくなるというリスクに常にさらされています。 パッケージのバージョンアップにより知らないうちに挙動が変わってしまい、 あなたのコードが動作しなくなる可能性もあります。そのようなことを避けるため、 「このパッケージはこのように動作するはず」 ということを常にテストケースで記述しておくようにします。テストが成功すれば、 期待通りに動作していることがわかります。もし動作仕様をすべてテストで記述できているのなら、 外部パッケージが将来バージョンアップされたとしても何の心配もいりません。 テストをクリアしたということは、システムは期待通りに動作するということだからです。
あるパッケージについての機能を文書化するためにテストを書いているとき、 そのテストの所有者はあなたです。今あなたがテストを作成しているパッケージの作者は、 そのテストのことについては何も知りません。パッケージの作者とよりつながりを深めるため、 作成したテストを使用してコミュニケートしたり、 そのテストを使用して共同作業をしたりすることができるでしょう。
あなたが作成したテストを使用してパッケージの作者と共同作業をすることになれば、 テストも共同で書くことになります。そうすることで、 より多くのテストケースを挙げられるようになるでしょう。 「暗黙の了解」などに頼っていては、共同作業はできません。 テストと同時に、あなたはそのパッケージに対して期待していることを正確に文書化することになります。 また、すべてのテストにクリアした時点で、 作者はパッケージが完成したことを知ることになります。
スタブ (本書の前のほうで説明した "モックオブジェクト" の章を参照ください) を使用することで、パッケージの作者と別れても作業できるようになります。 パッケージ作者の仕事は、パッケージの実際の実装でテストをクリアするようにすること。 そしてあなたの仕事はあなたが書いたコードでテストをクリアするようにすることです。 この段階になれば、あなたはスタブオブジェクトを使用すればよいのです。 このやり方により、2 つのチームが独立して開発できるようになります。
PHPUnit Skeleton Generator を使うと、 プロダクションコードのクラスからテストクラスを生成したり その逆を実行したりすることができます。 インストールするには、次のコマンドを実行します。
pear install phpunit/PHPUnit_SkeletonGenerator既存のコードのテストを記述する際は、 以下のようなコードを何度となく繰り返し記述することになるでしょう。
public function testMethod()
{
}PHPUnit Skeleton Generator は、既存のクラスのコードを解析して テストケースクラスの雛形を作成することができます。
次の例は、Calculator という名前のクラス
(例 16.1 を参照ください)
用のテストクラスの雛形を作成する手順を示すものです。
phpunit-skelgen --test Calculator
PHPUnit Skeleton Generator 1.0.0 by Sebastian Bergmann.
Wrote skeleton for "CalculatorTest" to "/home/sb/CalculatorTest.php".もとのクラスの各メソッドについて、 出来上がったテストケースクラスのテストケースは不完全な状態 (第 9 章 を参照ください) です。
名前空間 内で宣言されたクラス用のコードを雛形ジェネレータで生成する際には、 そのクラスの修飾名とクラスが宣言されているソースファイルへのパスを渡さなければなりません。
たとえば、名前空間 project の中でクラス
Calculator が宣言されている場合は
次のようにして雛形ジェネレータを実行します。
phpunit-skelgen --test -- "project\Calculator" Calculator.php
PHPUnit Skeleton Generator 1.0.0 by Sebastian Bergmann.
Wrote skeleton for "project\CalculatorTest" to "/home/sb/CalculatorTest.php".
作成されたテストケースクラスを実行した結果を以下に示します。
phpunit --bootstrap Calculator.php --verbose CalculatorTest
PHPUnit 3.7.0 by Sebastian Bergmann.
I
Time: 0 seconds, Memory: 3.50Mb
There was 1 incomplete test:
1) CalculatorTest::testAdd
This test has not been implemented yet.
/home/sb/CalculatorTest.php:38
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, 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 --bootstrap Calculator.php --verbose CalculatorTest
PHPUnit 3.7.0 by Sebastian Bergmann.
....
Time: 0 seconds, Memory: 3.50Mb
OK (4 tests, 4 assertions)
表 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(...)) |
@assert (...) > X | assertGreaterThan(X, method(...)) |
@assert (...) >= X | assertGreaterThanOrEqual(X, method(...)) |
@assert (...) < X | assertLessThan(X, method(...)) |
@assert (...) <= X | assertLessThanOrEqual(X, method(...)) |
@assert (...) throws X | @expectedException X |
テスト駆動開発 (第 12 章 を参照ください) ではまずテストを書いてからそのテストの対象となるコードを書くことになりますが、 PHPUnit ではテストケースクラスをもとにしてクラスの雛形を作成することができます。
規約に従って、Unit クラスのテストは
UnitTest クラスに記述されることになります。
このテストケースクラスのソースを検索し、
Unit クラスのオブジェクトを参照している変数を見つけて
そのオブジェクトがどんなメソッドをコールしているかを調べます。
例として 例 16.4
を見てみましょう。これは、例 16.3
の解析結果をもとにして作成されたものです。
例 16.3: BowlingGameTest クラス
<?php
class BowlingGameTest extends PHPUnit_Framework_TestCase
{
protected $game;
protected function setUp()
{
$this->game = new BowlingGame;
}
protected function rollMany($n, $pins)
{
for ($i = 0; $i < $n; $i++) {
$this->game->roll($pins);
}
}
public function testScoreForGutterGameIs0()
{
$this->rollMany(20, 0);
$this->assertEquals(0, $this->game->score());
}
}
?>
phpunit-skelgen --class BowlingGameTest
PHPUnit Skeleton Generator 1.0.0 by Sebastian Bergmann.
Wrote skeleton for "BowlingGame" to "./BowlingGame.php".例 16.4: 作成された BowlingGame クラスの雛形
<?php
/**
* Generated by PHPUnit_SkeletonGenerator on 2012-01-09 at 16:55:58.
*/
class BowlingGame
{
/**
* @todo Implement roll().
*/
public function roll()
{
// Remove the following line when you implement this method.
throw new RuntimeException('Not yet implemented.');
}
/**
* @todo Implement score().
*/
public function score()
{
// Remove the following line when you implement this method.
throw new RuntimeException('Not yet implemented.');
}
}
?>
作成されたクラスに対してテストを実行した結果を以下に示します。
phpunit --bootstrap BowlingGame.php BowlingGameTest
PHPUnit 3.7.0 by Sebastian Bergmann.
E
Time: 0 seconds, Memory: 3.50Mb
There was 1 error:
1) BowlingGameTest::testScoreForGutterGameIs0
RuntimeException: Not yet implemented.
/home/sb/BowlingGame.php:13
/home/sb/BowlingGameTest.php:14
/home/sb/BowlingGameTest.php:20
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.Selenium Server はテストツールのひとつです。これは、OS を通してブラウザのプロセスを動かし、 ブラウザのタスクを自動実行します。 あらゆるプログラミング言語で稼動しているウェブサイトに対応しており、 現在主流のあらゆるブラウザで使用することができます。Selenium RC は Selenium Core を使用しています。これは、ブラウザ上でのタスクを自動的に実行する JavaScript のライブラリです。Selenium でのテストは、 一般のユーザが使用するのと同じようにブラウザ上で直接実行されます。 主な使用例としては、受け入れテスト (各システム単体のテストではなく、結合されたシステム全体に対するテスト) や ブラウザの互換性のテスト (ウェブアプリケーションを、さまざまなオペレーティングシステムやブラウザでテストする) などがあります。
PHPUnit_Selenium がサポートしている唯一のシナリオは、 Selenium 2.x サーバを使うものです。 Selenium 2.x サーバにアクセスするには、1.0 から存在する古い形式の Selenium RC API を使うか、 あるいは PHPUnit_Selenium 1.2 で一部実装済みの WebDriver API を使います。
なぜそうしたかというと、Selenium 2 には後方互換性があり、 Selenium RC がもうメンテナンスされていないからです。
まず、Selenium Server をインストールします。
selenium-server-standalone-2.9.0.jar (バージョンをチェックすること) を /usr/local/bin などにコピーする。java -jar /usr/local/bin/selenium-server-standalone-2.9.0.jar などのようにして Selenium RC サーバを起動する。次に PHPUnit_Selenium パッケージをインストールします。これは、PHPUnit から Selenium Server へネイティブにアクセスするために必要です。
pear install phpunit/PHPUnit_Seleniumを実行しましょう。
これで、クライアント/サーバ プロトコルを用いて Selenium Server にコマンドを送信できるようになりました。
PHPUnit_Extensions_Selenium2TestCase テストケースは、
WebDriver API を利用します (実装しているのはその一部だけです)。
例 17.1 は、
ウェブサイト http://www.example.com/
の <title> 要素の内容をテストする方法を示したものです。
例 17.1: PHPUnit_Extensions_Selenium2TestCase の使用例
<?php
class WebTest extends PHPUnit_Extensions_Selenium2TestCase
{
protected function setUp()
{
$this->setBrowser('firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->url('http://www.example.com/');
$this->assertEquals('Example WWW Page', $this->title());
}
}
?>
phpunit WebTest
PHPUnit 3.6.10 by Sebastian Bergmann.
F
Time: 28 seconds, Memory: 3.00Mb
There was 1 failure:
1) WebTest::testTitle
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'Example WWW Page'
+'IANA — Example domains'
/home/giorgio/WebTest.php:13
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.Selenium2TestCare のコマンドは __call() を使って実装しています。 サポートする機能の一覧は PHPUnit_Extensions_Selenium2TestCase のエンドツーエンドテスト を参照ください。
PHPUnit_Extensions_SeleniumTestCase
は、Selenium Server と通信するための クライアント/サーバ プロトコルを実装したものです。
また、ウェブのテスト用に特化したアサーションメソッドも提供します。
例 17.2 は、
ウェブサイト http://www.example.com/
の <title> 要素の内容をテストする方法を示したものです。
例 17.2: 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->assertTitle('Example WWW Page');
}
}
?>
phpunit WebTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 9 seconds, Memory: 6.00Mb
There was 1 failure:
1) WebTest::testTitle
Current URL: http://www.iana.org/domains/example/
Failed asserting that 'IANA — Example domains' matches PCRE pattern "/Example WWW Page/".
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
PHPUnit_Framework_TestCase クラスとは異なり、
PHPUnit_Extensions_SeleniumTestCase を継承したテストケースクラスは
setUp() メソッドが必須となります。
このメソッド内で、Selenium Server セッションの設定を行います。
ここで使用できるメソッドの一覧は
表 17.1
を参照ください。
表17.1 Selenium Server API: セットアップ
| メソッド | 意味 |
|---|---|
void setBrowser(string $browser) | Selenium Server が使用するブラウザを設定します。 |
void setBrowserUrl(string $browserUrl) | テストするベース URL を設定します。 |
void setHost(string $host) | Selenium Server に接続する際のホスト名を設定します。 |
void setPort(int $port) | Selenium Server に接続する際のポートを設定します。 |
void setTimeout(int $timeout) | Selenium Server に接続する際のタイムアウト値を設定します。 |
void setSleep(int $seconds) | Selenium Server クライアントが、Selenium Server のサーバにアクションコマンドを送信してから待機する秒数を設定します。 |
PHPUnit では、Selenium のテストが失敗したときのスクリーンショットを撮ることができます。
この機能を使うには、$captureScreenshotOnFailure、
$screenshotPath および $screenshotUrl
をテストケースクラス内で
例 17.3
のように指定します。
例 17.3: テストに失敗したときのスクリーンショットの取得
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected $captureScreenshotOnFailure = TRUE;
protected $screenshotPath = '/var/www/localhost/htdocs/screenshots';
protected $screenshotUrl = 'http://localhost/screenshots';
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example WWW Page');
}
}
?>
phpunit WebTest
PHPUnit 3.7.0 by Sebastian Bergmann.
F
Time: 7 seconds, Memory: 6.00Mb
There was 1 failure:
1) WebTest::testTitle
Current URL: http://www.iana.org/domains/example/
Screenshot: http://localhost/screenshots/334b080f2364b5f11568ee1c7f6742c9.png
Failed asserting that 'IANA — Example domains' matches PCRE pattern "/Example WWW Page/".
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
複数のブラウザを使用してテストを行なうこともできます。この場合は、
setBrowser() でブラウザの設定を行うかわりに、
テストケースクラスの中で $browsers という名前の
public static な配列を作成します。
この配列の各項目が個々のブラウザの設定を表します。
これらのブラウザは、それぞれ別の Selenium Server のサーバで管理することができます。
例 17.4
に例を示します。
例 17.4: 複数のブラウザの設定管理
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
public static $browsers = array(
array(
'name' => 'Firefox on Linux',
'browser' => '*firefox',
'host' => 'my.linux.box',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Safari on MacOS X',
'browser' => '*safari',
'host' => 'my.macosx.box',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Safari on Windows XP',
'browser' => '*custom C:\Program Files\Safari\Safari.exe -url',
'host' => 'my.windowsxp.box',
'port' => 4444,
'timeout' => 30000,
),
array(
'name' => 'Internet Explorer on Windows XP',
'browser' => '*iexplore',
'host' => 'my.windowsxp.box',
'port' => 4444,
'timeout' => 30000,
)
);
protected function setUp()
{
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitle('Example Web Page');
}
}
?>
PHPUnit_Extensions_SeleniumTestCase を使用すると、
Selenium で実行したテストのカバレッジ情報を収集することができます。
PHPUnit/Extensions/SeleniumCommon/phpunit_coverage.php をウェブサーバのドキュメントルートディレクトリにコピーします。php.ini ファイルで、PHPUnit/Extensions/SeleniumCommon/prepend.php と PHPUnit/Extensions/SeleniumCommon/append.php をそれぞれ auto_prepend_file および auto_append_file に設定します。PHPUnit_Extensions_SeleniumTestCase を継承したテストケースクラスで、protected $coverageScriptUrl = 'http://host/phpunit_coverage.php'; のようにして phpunit_coverage.php スクリプトの URL を指定します。
表 17.2
は、PHPUnit_Extensions_SeleniumTestCase
が提供するさまざまなアサーションメソッドの一覧です。
表17.2 アサーション
| アサーション | 意味 |
|---|---|
void assertElementValueEquals(string $locator, string $text) | $locator で表される要素の値が $text と異なる場合にエラーを報告します。 |
void assertElementValueNotEquals(string $locator, string $text) | $locator で表される要素の値が $text と等しい場合にエラーを報告します。 |
void assertElementValueContains(string $locator, string $text) | $locator で表される要素の値が $text を含まない場合にエラーを報告します。 |
void assertElementValueNotContains(string $locator, string $text) | $locator で表される要素の値が $text を含む場合にエラーを報告します。 |
void assertElementContainsText(string $locator, string $text) | $locator で表される要素が $text を含まない場合にエラーを報告します。 |
void assertElementNotContainsText(string $locator, string $text) | $locator で表される要素が $text を含む場合にエラーを報告します。 |
void assertSelectHasOption(string $selectLocator, string $option) | 指定したオプションが使用できない場合にエラーを報告します。 |
void assertSelectNotHasOption(string $selectLocator, string $option) | 指定したオプションが使用できる場合にエラーを報告します。 |
void assertSelected($selectLocator, $option) | 指定したラベルが選択されていない場合にエラーを報告します。 |
void assertNotSelected($selectLocator, $option) | 指定したラベルが選択されている場合にエラーを報告します。 |
void assertIsSelected(string $selectLocator, string $value) | 指定した値が選択されていない場合にエラーを報告します。 |
void assertIsNotSelected(string $selectLocator, string $value) | 指定した値が選択されている場合にエラーを報告します。 |
表 17.3 は、
PHPUnit_Extensions_SeleniumTestCase
のテンプレートメソッドをまとめたものです。
表17.3 テンプレートメソッド
| メソッド | 意味 |
|---|---|
void defaultAssertions() | テストケース内のすべてのテストで共有するアサーションを上書きします。 このメソッドは、Selenium Server のサーバにコマンドが送信されるたびに (送信された後に) コールされます。 |
使用できるコマンドのリファレンスや実際の使用法については Selenium のドキュメント を参照ください。
Selenium 1 のコマンドは、__call で動的に実装されています。 PHPUnit_Extensions_SeleniumTestCase_Driver::__call() の API ドキュメント に、PHP 側で対応しているすべてのメソッドの一覧があります。 また、引数や返り値の型も確認できます。
runSelenese($filename) メソッドを使用すると、
Selenese/HTML の設定から Selenium のテストを実行することができます。
さらに、静的属性 $seleneseDirectory を使用すると、
Selenese/HTML ファイルを含むディレクトリから自動的にテストオブジェクトを作成することができます。
指定したディレクトリ配下を再帰的に走査し、
.htm ファイルを探します。このファイルには
Selenese/HTML が含まれているものとします。例として
例 17.5
を参照ください。
例 17.5: Selenese/HTML ファイルのディレクトリをテストとして使用する
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class SeleneseTests extends PHPUnit_Extensions_SeleniumTestCase
{
public static $seleneseDirectory = '/path/to/files';
}
?>
Selenium 1.1.1 から取り込まれた実験的な機能として、ユーザーが複数のテストでセッションを共有できるようになりました。
現在サポートしているのは、ひとつのブラウザを使うときに全テストでセッションを共有するという場合だけです。
セッションの共有機能を使うには、ブートストラップファイルで
PHPUnit_Extensions_SeleniumTestCase::shareSession(true) をコールします。
テストが成功しなかった (失敗、あるいは不完全) 場合は、共有セッションがリセットされます。
クッキーをリセットしたり、(tearDown() メソッドで) テスト対象のアプリケーションからログアウトしたりして
テストがお互い干渉しあわないようにするのは、ユーザ側の責任となります。
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"
assertions="2"
failures="0"
errors="0"
time="0.016030">
<testcase name="testNewArrayIsEmpty"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="6"
assertions="1"
time="0.008044"/>
<testcase name="testArrayContainsAnElement"
class="ArrayTest"
file="/home/sb/ArrayTest.php"
line="15"
assertions="1"
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"
assertions="1"
failures="1"
errors="1"
time="0.019744">
<testcase name="testFailure"
class="FailureErrorTest"
file="/home/sb/FailureErrorTest.php"
line="6"
assertions="1"
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"
assertions="0"
time="0.008288">
<error type="Exception">testError(FailureErrorTest)
Exception:
/home/sb/FailureErrorTest.php:13
</error>
</testcase>
</testsuite>
</testsuites>
Test Anything Protocol (TAP)
は、Perl のモジュールをテストする際に使用する、
シンプルなテキストベースのインターフェイスです。
以下の例は、ArrayTest のテストが生成した
TAP ログファイルです。
TAP version 13 ok 1 - testNewArrayIsEmpty(ArrayTest) ok 2 - testArrayContainsAnElement(ArrayTest) 1..2
次の TAP ログファイルは、テストクラス
FailureErrorTest にあるメソッド
testFailure および testError
が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。
TAP version 13
not ok 1 - Failure: testFailure(FailureErrorTest)
---
message: 'Failed asserting that <integer:2> matches expected value <integer:1>.'
severity: fail
data:
got: 2
expected: 1
...
not ok 2 - Error: testError(FailureErrorTest)
1..2
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.0082459449768066,"trace":[],
"message":"Failed asserting that <integer:2> is equal to <integer:1>."}
{"event":"test","suite":"FailureErrorTest",
"test":"testError(FailureErrorTest)","status":"error",
"time":0.0083680152893066,"trace":[],"message":""}
PHPUnit がコードカバレッジ情報のログ出力の際に使用している XML のフォーマットは、
Clover
のものを参考にしています。
以下の例は、BankAccountTest のテストが生成した
XML ログファイルです。
<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1184835473" phpunit="3.6.0">
<project name="BankAccountTest" timestamp="1184835473">
<file name="/home/sb/BankAccount.php">
<class name="BankAccountException">
<metrics methods="0" coveredmethods="0" statements="0"
coveredstatements="0" elements="0" coveredelements="0"/>
</class>
<class name="BankAccount">
<metrics methods="4" coveredmethods="4" statements="13"
coveredstatements="5" elements="17" coveredelements="9"/>
</class>
<line num="77" type="method" count="3"/>
<line num="79" type="stmt" count="3"/>
<line num="89" type="method" count="2"/>
<line num="91" type="stmt" count="2"/>
<line num="92" type="stmt" count="0"/>
<line num="93" type="stmt" count="0"/>
<line num="94" type="stmt" count="2"/>
<line num="96" type="stmt" count="0"/>
<line num="105" type="method" count="1"/>
<line num="107" type="stmt" count="1"/>
<line num="109" type="stmt" count="0"/>
<line num="119" type="method" count="1"/>
<line num="121" type="stmt" count="1"/>
<line num="123" type="stmt" count="0"/>
<metrics loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4"
statements="13" coveredstatements="5" elements="17"
coveredelements="9"/>
</file>
<metrics files="1" loc="126" ncloc="37" classes="2" methods="4"
coveredmethods="4" statements="13" coveredstatements="5"
elements="17" coveredelements="9"/>
</project>
</coverage>
人間が読める形式のコードカバレッジ情報を、コマンドラインあるいはテキストファイルに出力します。
この出力フォーマットの狙いは、ちょっとしたクラス群のカバレッジの概要を手軽に把握することです。
大規模なプロジェクトでは、このフォーマットを使えばプロジェクト全体のカバレッジを大まかに把握しやすくなるでしょう。
--filter と組み合わせて使うこともできます。
コマンドラインから使う場合は php://stdout に書き込みます。
この出力は --colors の設定を反映したものになります。
コマンドラインから使った場合は、デフォルトの出力先は標準出力となります。
デフォルトでは、テストで少なくとも一行はカバーしているファイルしか表示しません。
この設定は、xml の showUncoveredFiles オプションでしか変更できません。
「ログ出力」 を参照ください。
テストを書きやすくする、あるいはテストの実行結果の表示方法を変更するなど、 PHPUnit はさまざまな方法で拡張することができます。 PHPUnit を拡張するための第一歩をここで説明します。
PHPUnit_Framework_TestCase
を継承した抽象サブクラスにカスタムアサーションやユーティリティメソッドを書き、
そのクラスをさらに継承してテストクラスを作成します。
これが、PHPUnit を拡張するための一番簡単な方法です。
カスタムアサーションを作成するときには、PHPUnit 自体のアサーションの実装方法を真似るのがおすすめです。
例 19.1 を見ればわかるとおり、
assertTrue() メソッドは
isTrue() および assertThat() メソッドの単なるラッパーに過ぎません。
isTrue() が matcher オブジェクトを作り、それを
assertThat() に渡して評価しています。
例 19.1: PHPUnit_Framework_Assert クラスの assertTrue() および isTrue() メソッド
<?php
abstract class PHPUnit_Framework_Assert
{
// ...
/**
* Asserts that a condition is true.
*
* @param boolean $condition
* @param string $message
* @throws PHPUnit_Framework_AssertionFailedError
*/
public static function assertTrue($condition, $message = '')
{
self::assertThat($condition, self::isTrue(), $message);
}
// ...
/**
* Returns a PHPUnit_Framework_Constraint_IsTrue matcher object.
*
* @return PHPUnit_Framework_Constraint_IsTrue
* @since Method available since Release 3.3.0
*/
public static function isTrue()
{
return new PHPUnit_Framework_Constraint_IsTrue;
}
// ...
}?>
例 19.2 は、
PHPUnit_Framework_Constraint_IsTrue が
matcher オブジェクト (あるいは制約) のために抽象クラス
PHPUnit_Framework_Constraint を継承している部分です。
例 19.2: PHPUnit_Framework_Constraint_IsTrue クラス
<?php
class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint
{
/**
* Evaluates the constraint for parameter $other. Returns TRUE if the
* constraint is met, FALSE otherwise.
*
* @param mixed $other Value or object to evaluate.
* @return bool
*/
public function matches($other)
{
return $other === TRUE;
}
/**
* Returns a string representation of the constraint.
*
* @return string
*/
public function toString()
{
return 'is true';
}
}?>
assertTrue() や
isTrue() メソッドの実装を
PHPUnit_Framework_Constraint_IsTrue クラスと同じようにしておけば、
アサーションの評価やタスクの記録 (テストの統計情報に自動的に更新するなど)
を assertThat() が自動的に行ってくれるようになります。
さらに、モックオブジェクトを設定する際の matcher として isTrue()
メソッドを使えるようにもなります。
例 19.3 は、
PHPUnit_Framework_TestListener
インターフェイスのシンプルな実装例です。
例 19.3: シンプルなテストリスナー
<?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());
}
}
?>
「テストリスナー」 に、自作のテストリスナーをテスト実行時にアタッチするための PHPUnit の設定方法についての説明があります。
PHPUnit_Extensions_TestDecorator
のサブクラスでテストケースあるいはテストスイートをラッピングし、
デコレータパターンを使用することで
各テストの実行前後に何らかの処理をさせることができます。
PHPUnit には、PHPUnit_Extensions_RepeatedTest
および PHPUnit_Extensions_TestSetup
という 2 つの具象テストデコレータが付属しています。
前者はテストを繰り返し実行し、それらが全て成功した場合にのみ成功とみなします。
後者については 第 6 章 で説明しました。
例 19.4
は、テストデコレータ PHPUnit_Extensions_RepeatedTest
の一部を抜粋したものです。独自のデコレータを作成するための参考にしてください。
例 19.4: 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) ファイルの値と比較する、データ駆動のテストを
例 19.5
に示します。このファイルの各行は foo;bar
のような形式になっており (訳注: CSV じゃない……)、
最初の値が期待値で 2 番目の値が実際の値です。
例 19.5: データ駆動のテスト
<?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);
PHP_Timer::start();
$stopTime = NULL;
list($expected, $actual) = explode(';', $line);
try {
PHPUnit_Framework_Assert::assertEquals(
trim($expected), trim($actual)
);
}
catch (PHPUnit_Framework_AssertionFailedError $e) {
$stopTime = PHP_Timer::stop();
$result->addFailure($this, $e, $stopTime);
}
catch (Exception $e) {
$stopTime = PHP_Timer::stop();
$result->addError($this, $e, $stopTime);
}
if ($stopTime === NULL) {
$stopTime = PHP_Timer::stop();
}
$result->endTest($this, $stopTime);
}
return $result;
}
}
$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 3.7.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.
表 A.1 に、すべてのアサーションを示します。
表A.1 アサーション
| アサーション |
|---|
assertArrayHasKey($key, $array, $message = '') |
assertArrayNotHasKey($key, $array, $message = '') |
assertAttributeContains($needle, $haystackAttributeName, $haystackClassOrObject, $message = '', $ignoreCase = FALSE, $checkForObjectIdentity = TRUE) |
assertAttributeContainsOnly($type, $haystackAttributeName, $haystackClassOrObject, $isNativeType = NULL, $message = '') |
assertAttributeCount($expectedCount, $haystackAttributeName, $haystackClassOrObject, $message = '') |
assertAttributeEmpty($haystackAttributeName, $haystackClassOrObject, $message = '') |
assertAttributeEquals($expected, $actualAttributeName, $actualClassOrObject, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) |
assertAttributeGreaterThan($expected, $actualAttributeName, $actualClassOrObject, $message = '') |
assertAttributeGreaterThanOrEqual($expected, $actualAttributeName, $actualClassOrObject, $message = '') |
assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') |
assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') |
assertAttributeLessThan($expected, $actualAttributeName, $actualClassOrObject, $message = '') |
assertAttributeLessThanOrEqual($expected, $actualAttributeName, $actualClassOrObject, $message = '') |
assertAttributeNotContains($needle, $haystackAttributeName, $haystackClassOrObject, $message = '', $ignoreCase = FALSE, $checkForObjectIdentity = TRUE) |
assertAttributeNotContainsOnly($type, $haystackAttributeName, $haystackClassOrObject, $isNativeType = NULL, $message = '') |
assertAttributeNotCount($expectedCount, $haystackAttributeName, $haystackClassOrObject, $message = '') |
assertAttributeNotEmpty($haystackAttributeName, $haystackClassOrObject, $message = '') |
assertAttributeNotEquals($expected, $actualAttributeName, $actualClassOrObject, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) |
assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') |
assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') |
assertAttributeNotSame($expected, $actualAttributeName, $actualClassOrObject, $message = '') |
assertAttributeSame($expected, $actualAttributeName, $actualClassOrObject, $message = '') |
assertClassHasAttribute($attributeName, $className, $message = '') |
assertClassHasStaticAttribute($attributeName, $className, $message = '') |
assertClassNotHasAttribute($attributeName, $className, $message = '') |
assertClassNotHasStaticAttribute($attributeName, $className, $message = '') |
assertContains($needle, $haystack, $message = '', $ignoreCase = FALSE, $checkForObjectIdentity = TRUE) |
assertContainsOnly($type, $haystack, $isNativeType = NULL, $message = '') |
assertContainsOnlyInstancesOf($classname, $haystack, $message = '') |
assertCount($expectedCount, $haystack, $message = '') |
assertEmpty($actual, $message = '') |
assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement, $checkAttributes = FALSE, $message = '') |
assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) |
assertFalse($condition, $message = '') |
assertFileEquals($expected, $actual, $message = '', $canonicalize = FALSE, $ignoreCase = FALSE) |
assertFileExists($filename, $message = '') |
assertFileNotEquals($expected, $actual, $message = '', $canonicalize = FALSE, $ignoreCase = FALSE) |
assertFileNotExists($filename, $message = '') |
assertGreaterThan($expected, $actual, $message = '') |
assertGreaterThanOrEqual($expected, $actual, $message = '') |
assertInstanceOf($expected, $actual, $message = '') |
assertInternalType($expected, $actual, $message = '') |
assertJsonFileEqualsJsonFile($expectedFile, $actualFile, $message = '') |
assertJsonFileNotEqualsJsonFile($expectedFile, $actualFile, $message = '') |
assertJsonStringEqualsJsonFile($expectedFile, $actualJson, $message = '') |
assertJsonStringEqualsJsonString($expectedJson, $actualJson, $message = '') |
assertJsonStringNotEqualsJsonFile($expectedFile, $actualJson, $message = '') |
assertJsonStringNotEqualsJsonString($expectedJson, $actualJson, $message = '') |
assertLessThan($expected, $actual, $message = '') |
assertLessThanOrEqual($expected, $actual, $message = '') |
assertNotContains($needle, $haystack, $message = '', $ignoreCase = FALSE, $checkForObjectIdentity = TRUE) |
assertNotContainsOnly($type, $haystack, $isNativeType = NULL, $message = '') |
assertNotCount($expectedCount, $haystack, $message = '') |
assertNotEmpty($actual, $message = '') |
assertNotEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) |
assertNotInstanceOf($expected, $actual, $message = '') |
assertNotInternalType($expected, $actual, $message = '') |
assertNotNull($actual, $message = '') |
assertNotRegExp($pattern, $string, $message = '') |
assertNotSame($expected, $actual, $message = '') |
assertNotSameSize($expected, $actual, $message = '') |
assertNotTag($matcher, $actual, $message = '', $isHtml = TRUE) |
assertNull($actual, $message = '') |
assertObjectHasAttribute($attributeName, $object, $message = '') |
assertObjectNotHasAttribute($attributeName, $object, $message = '') |
assertRegExp($pattern, $string, $message = '') |
assertSame($expected, $actual, $message = '') |
assertSameSize($expected, $actual, $message = '') |
assertSelectCount($selector, $count, $actual, $message = '', $isHtml = TRUE) |
assertSelectEquals($selector, $content, $count, $actual, $message = '', $isHtml = TRUE) |
assertSelectRegExp($selector, $pattern, $count, $actual, $message = '', $isHtml = TRUE) |
assertStringEndsNotWith($suffix, $string, $message = '') |
assertStringEndsWith($suffix, $string, $message = '') |
assertStringEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = FALSE, $ignoreCase = FALSE) |
assertStringMatchesFormat($format, $string, $message = '') |
assertStringMatchesFormatFile($formatFile, $string, $message = '') |
assertStringNotEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = FALSE, $ignoreCase = FALSE) |
assertStringNotMatchesFormat($format, $string, $message = '') |
assertStringNotMatchesFormatFile($formatFile, $string, $message = '') |
assertStringStartsNotWith($prefix, $string, $message = '') |
assertStringStartsWith($prefix, $string, $message = '') |
assertTag($matcher, $actual, $message = '', $isHtml = TRUE) |
assertThat($value, PHPUnit_Framework_Constraint $constraint, $message = '') |
assertTrue($condition, $message = '') |
assertXmlFileEqualsXmlFile($expectedFile, $actualFile, $message = '') |
assertXmlFileNotEqualsXmlFile($expectedFile, $actualFile, $message = '') |
assertXmlStringEqualsXmlFile($expectedFile, $actualXml, $message = '') |
assertXmlStringEqualsXmlString($expectedXml, $actualXml, $message = '') |
assertXmlStringNotEqualsXmlFile($expectedFile, $actualXml, $message = '') |
assertXmlStringNotEqualsXmlString($expectedXml, $actualXml, $message = '') |
アノテーションとはメタデータを表す特別な構文のことで、
プログラミング言語のソースコードに追加することができます。
PHP そのものにはソースコードにアノテーションする専用の仕組みはありませんが、
ドキュメンテーションブロックに @アノテーション名 引数
のようなタグを書くことでアノテーションを表すという記法が
PHP コミュニティ内で一般に使われています。
PHP では、リフレクション API の getDocComment()
メソッドを使えば関数、クラス、メソッド、属性
それぞれのドキュメンテーションブロックにアクセスすることができます。
PHPUnit などのアプリケーションでは、
この情報をもとに実行時の振る舞いを設定するのです。
本章では、PHPUnit がサポートするすべてのアノテーションについて解説します。
@author アノテーションは
@group アノテーション (「@group」 を参照ください) のエイリアスで、
テストの作者にもとづいたフィルタリングができるようになります。
グローバル変数の保存や復元を、テストケースクラスのすべてのテストで完全に無効にすることができます。 このように使います。
/**
* @backupGlobals disabled
*/
class MyTest extends PHPUnit_Framework_TestCase
{
// ...
}
@backupGlobals アノテーションは、テストメソッドレベルで使うこともできます。
これによって、保存と復元の操作をより細やかに制御できるようになります。
/**
* @backupGlobals disabled
*/
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @backupGlobals enabled
*/
public function testThatInteractsWithGlobalVariables()
{
// ...
}
}
クラスの静的属性の保存や復元を、テストケースクラスのすべてのテストで完全に無効にすることができます。 このように使います。
/**
* @backupStaticAttributes disabled
*/
class MyTest extends PHPUnit_Framework_TestCase
{
// ...
}
@backupStaticAttributes アノテーションは、テストメソッドレベルで使うこともできます。
これによって、保存と復元の操作をより細やかに制御できるようになります。
/**
* @backupStaticAttributes disabled
*/
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @backupStaticAttributes enabled
*/
public function testThatInteractsWithStaticAttributes()
{
// ...
}
}
@codeCoverageIgnore や
@codeCoverageIgnoreStart、そして
@codeCoverageIgnoreEnd アノテーションを使うと、
コード内の特定の行をカバレッジ解析の対象外にできます。
利用法は 「コードブロックの無視」 を参照ください。
@covers アノテーションをテストコードで使うと、
そのテストメソッドがどのメソッドをテストするのかを指定することができます。
/**
* @covers BankAccount::getBalance
*/
public function testBalanceIsInitiallyZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
これを指定した場合は、指定したメソッドのみのコードカバレッジ情報を考慮することになります。
表 B.1
に @covers アノテーションの構文を示します。
表B.1 カバーするメソッドを指定するためのアノテーション
| アノテーション | 説明 |
|---|---|
@covers ClassName::methodName | そのテストメソッドが指定したメソッドをカバーすることを表します。 |
@covers ClassName | そのテストメソッドが指定したクラスのすべてのメソッドをカバーすることを表します。 |
@covers ClassName<extended> | そのテストメソッドが、指定したクラスとその親クラスおよびインターフェイスのすべてのメソッドをカバーすることを表します。 |
@covers ClassName::<public> | そのテストメソッドが、指定したクラスのすべての public メソッドをカバーすることを表します。 |
@covers ClassName::<protected> | そのテストメソッドが、指定したクラスのすべての protected メソッドをカバーすることを表します。 |
@covers ClassName::<private> | そのテストメソッドが、指定したクラスのすべての private メソッドをカバーすることを表します。 |
@covers ClassName::<!public> | そのテストメソッドが、指定したクラスのすべての非 public メソッドをカバーすることを表します。 |
@covers ClassName::<!protected> | そのテストメソッドが、指定したクラスのすべての非 protected メソッドをカバーすることを表します。 |
@covers ClassName::<!private> | そのテストメソッドが、指定したクラスのすべての非 private メソッドをカバーすることを表します。 |
@covers ::functionName | そのテストメソッドが、指定したグローバル関数をカバーすることを表します。 |
@coversNothing アノテーションをテストコードで使うと、
そのテストケースについてはコードカバレッジ情報を記録しないように指定できます。
これはインテグレーションテストで使えます。例として 例 14.3 を参照ください。
このメソッドはクラスレベルおよびメソッドレベルで使え、
あらゆる @covers タグを上書きします。
テストメソッドには任意の引数を渡すことができます。
引数は、データプロバイダメソッド
(例 4.4 の
provider()) から渡されます。
使用するデータプロバイダメソッドを指定するには
@dataProvider アノテーションを使います。
詳細は 「データプロバイダ」 を参照ください。
PHPUnit は、テストメソッド間の依存性の明示的な宣言をサポートしています。
この依存性とは、テストメソッドが実行される順序を定義するものではありません。
プロデューサーがテストフィクスチャを作ってそのインスタンスを返し、
依存するコンシューマーがそれを受け取って利用するというものです。
例 4.2
は、@depends アノテーションを使ってテストメソッドの依存性をあらわす例です。
詳細は 「テストの依存性」 を参照ください。
@expectedExceptionCode アノテーションを
@expectedException と組み合わせて使うと、
スローされた例外のエラーコードについてのアサーションが可能となり、
例外をより狭い範囲に特定できるようになります。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MyException
* @expectedExceptionCode 20
*/
public function testExceptionHasErrorcode20()
{
throw new MyException('Some Message', 20);
}
}
テストを実行しやすくし、重複を減らすために、
ショートカットを使ってクラス定数を指定することができます。
@expectedExceptionCode で
"@expectedExceptionCode ClassName::CONST" のようにして使います。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MyException
* @expectedExceptionCode MyClass::ERRORCODE
*/
public function testExceptionHasErrorcode20()
{
throw new MyException('Some Message', 20);
}
}
class MyClass
{
const ERRORCODE = 20;
}
@expectedExceptionMessage アノテーションは
@expectedExceptionCode と似ており、
例外のエラーメッセージに関するアサーションを行います。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MyException
* @expectedExceptionMessage Some Message
*/
public function testExceptionHasRightMessage()
{
throw new MyException('Some Message', 20);
}
}期待するメッセージを、例外メッセージの一部にすることもできます。 これは、特定の名前や渡したパラメータが例外に表示されることを確かめたいけれども 例外メッセージ全体は固定していない場合に便利です。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException MyException
* @expectedExceptionMessage broken
*/
public function testExceptionHasRightMessage()
{
$param = "broken";
throw new MyException('Invalid parameter "'.$param.'".', 20);
}
}
テストを実行しやすくし、重複を減らすために、
ショートカットを使ってクラス定数を指定することができます。
@expectedExceptionMessage で
"@expectedExceptionMessage ClassName::CONST" のようにして使います。
サンプルコードは 「@expectedExceptionCode」 を参照ください。
あるテストを、ひとつあるいは複数のグループに属するものとすることができます。
@group アノテーションをこのように使用します。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @group specification
*/
public function testSomething()
{
}
/**
* @group regresssion
* @group bug2204
*/
public function testSomethingElse()
{
}
}
特定のグループに属するテストのみを選んで実行するには、
コマンドラインのテストランナーの場合は
--group スイッチあるいは --exclude-group
スイッチを指定します。XML 設定ファイルの場合は、
それぞれ対応するディレクティブを指定します。
@outputBuffering アノテーションを使うと、PHP の
出力バッファリング
をこのように制御することができます。
/**
* @outputBuffering enabled
*/
class MyTest extends PHPUnit_Framework_TestCase
{
// ...
}
@outputBuffering アノテーションは、テストメソッドレベルで使うこともできます。
これによって、出力バッファリングをより細やかに制御できるようになります。
/**
* @outputBuffering disabled
*/
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @outputBuffering enabled
*/
public function testThatPrintsSomething()
{
// ...
}
}
テストを別プロセスで実行するときに、
PHPUnit は親プロセスのグローバルな状態を保存しようと試みます。
親プロセスのすべてのグローバル状態をシリアライズし、
子プロセス内で最後にそれをアンシリアライズするのです。
しかし、親プロセスのグローバル状態の中にもしシリアライズできないものがあれば、
問題が発生します。この問題に対応するために、グローバル状態の保存を無効にすることができます。
そのために使うのが @preserveGlobalState アノテーションです。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testInSeparateProcess()
{
// ...
}
}
@requires アノテーションを使うと、共通の事前条件
(たとえば PHP のバージョンや拡張モジュールのインストール状況)
を満たさないときにテストをスキップできます。
条件に指定できる内容やその例については 表 9.3 を参照ください。
テストクラス内のすべてのテストケースを、個別の PHP プロセスで実行するように指示します。
/**
* @runTestsInSeparateProcesses
*/
class MyTest extends PHPUnit_Framework_TestCase
{
// ...
}
注意:
デフォルトで、PHPUnit は親プロセスのグローバルな状態を保存しようと試みます。
親プロセスのすべてのグローバル状態をシリアライズし、
子プロセス内で最後にそれをアンシリアライズするのです。
しかし、親プロセスのグローバル状態の中にもしシリアライズできないものがあれば、
問題が発生します。この問題に対応するために、グローバル状態の保存を無効にすることができます。
この問題の対処法については 「@preserveGlobalState」
を参照ください。
そのテストを個別の PHP プロセスで実行するように指示します。
class MyTest extends PHPUnit_Framework_TestCase
{
/**
* @runInSeparateProcess
*/
public function testInSeparateProcess()
{
// ...
}
}
注意:
デフォルトで、PHPUnit は親プロセスのグローバルな状態を保存しようと試みます。
親プロセスのすべてのグローバル状態をシリアライズし、
子プロセス内で最後にそれをアンシリアライズするのです。
しかし、親プロセスのグローバル状態の中にもしシリアライズできないものがあれば、
問題が発生します。この問題に対応するために、グローバル状態の保存を無効にすることができます。
この問題の対処法については 「@preserveGlobalState」
を参照ください。
<phpunit> 要素の属性を使って
PHPUnit のコア機能を設定します。
<phpunit backupGlobals="true"
backupStaticAttributes="false"
<!--bootstrap="/path/to/bootstrap.php"-->
cacheTokens="false"
colors="false"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
forceCoversAnnotation="false"
mapTestClassNameToCoveredClassName="false"
printerClass="PHPUnit_TextUI_ResultPrinter"
<!--printerFile="/path/to/ResultPrinter.php"-->
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
<!--testSuiteLoaderFile="/path/to/StandardTestSuiteLoader.php"-->
strict="false"
verbose="false">
<!-- ... -->
</phpunit>上の XML 設定ファイルは、TextUI テストランナーをデフォルトの設定で起動します。 その詳細は 「Command-Line switches」 で説明します。
その他、コマンドラインからは設定できないオプションもあります。
convertErrorsToExceptions
false にすると、
すべての PHP のエラーを例外に変換するエラーハンドラをインストールしません。
convertNoticesToExceptions
false にすると、
convertErrorsToExceptions でインストールしたエラーハンドラが
E_NOTICE や E_USER_NOTICE そして
E_STRICT を例外に変換しなくなります。
convertWarningsToExceptions
false にすると、
convertErrorsToExceptions でインストールしたエラーハンドラが
E_WARNING や E_USER_WARNING
を例外に変換しなくなります。
forceCoversAnnotation
コードカバレッジの記録を、
@covers アノテーションを使っている関数だけに限定します。
このアノテーションについては
「@covers」 で説明します。
<testsuites> 要素とその子要素である
<testsuite> を使って、
テストスイート群やテストケース群の中からテストスイートを構成します。
<testsuites>
<testsuite name="My Test Suite">
<directory>/path/to/*Test.php files</directory>
<file>/path/to/MyTest.php</file>
<exclude>/path/to/exclude</exclude>
</testsuite>
</testsuites>
phpVersion および
phpVersionOperator 属性を使うと、必要な PHP
のバージョンを指定できます。次の例は、PHP のバージョンが 5.3.0 以降である場合にのみ
/path/to/*Test.php と
/path/to/MyTest.php を追加します。
<testsuites>
<testsuite name="My Test Suite">
<directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/path/to/files</directory>
<file phpVersion="5.3.0" phpVersionOperator=">=">/path/to/MyTest.php</file>
</testsuite>
</testsuites>
phpVersionOperator 属性はオプションで、デフォルトは
>= です。
<groups> 要素とその子要素である
<include>、
<exclude> および
<group> を使って、
テストスイートの中から実行する (しない) テストグループを選びます。
<groups>
<include>
<group>name</group>
</include>
<exclude>
<group>name</group>
</exclude>
</groups>上の XML 設定ファイルは、 TextUI テストランナーを以下の引数で起動します。
--group name
--exclude-group name
<filter> 要素とその子要素を使って、
コードカバレッジレポートのブラックリストとホワイトリストを設定します。
<filter>
<blacklist>
<directory suffix=".php">/path/to/files</directory>
<file>/path/to/file</file>
<exclude>
<directory suffix=".php">/path/to/files</directory>
<file>/path/to/file</file>
</exclude>
</blacklist>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">/path/to/files</directory>
<file>/path/to/file</file>
<exclude>
<directory suffix=".php">/path/to/files</directory>
<file>/path/to/file</file>
</exclude>
</whitelist>
</filter>
<logging> 要素とその子要素である
<log> を使って、
テストの実行結果のログ出力を設定します。
<logging>
<log type="coverage-html" target="/tmp/report" charset="UTF-8"
highlight="false" lowUpperBound="35" highLowerBound="70"/>
<log type="coverage-clover" target="/tmp/coverage.xml"/>
<log type="coverage-php" target="/tmp/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
<log type="json" target="/tmp/logfile.json"/>
<log type="tap" target="/tmp/logfile.tap"/>
<log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/>
<log type="testdox-html" target="/tmp/testdox.html"/>
<log type="testdox-text" target="/tmp/testdox.txt"/>
</logging>上の XML 設定ファイルは、 TextUI テストランナーを以下の引数で起動します。
--coverage-html /tmp/report
--coverage-clover /tmp/coverage.xml
--coverage-php /tmp/coverage.serialized
--coverage-text
--log-json /tmp/logfile.json
> /tmp/logfile.txt
--log-tap /tmp/logfile.tap
--log-junit /tmp/logfile.xml
--testdox-html /tmp/testdox.html
--testdox-text /tmp/testdox.txt
charset、highlight、
lowUpperBound、highLowerBound、
logIncompleteSkipped
および showUncoveredFiles
属性には、TextUI テストランナーで対応するスイッチがありません。
charset: 生成する html ページで使う文字セット。
highlight: true にすると、カバレッジレポート内のコードにシンタックスハイライト処理を施します。
lowUpperBound: カバー率がこの値に満たないときに、カバー率が "低い" とみなします。
highLowerBound: カバー率がこの値を超えるときに、カバー率が "高い" とみなします。
showUncoveredFiles: --coverage-text の出力で、カバレッジ情報だけではなくホワイトリストの全ファイル一覧も表示します。
<listeners> 要素とその子要素である
<listener> を使って、
テスト実行時にテストリスナーをアタッチします。
<listeners>
<listener class="MyListener" file="/optional/path/to/MyListener.php">
<arguments>
<array>
<element key="0">
<string>Sebastian</string>
</element>
</array>
<integer>22</integer>
<string>April</string>
<double>19.78</double>
<null/>
<object class="stdClass"/>
</arguments>
</listener>
</listeners>
上の XML 設定は、
$listener オブジェクト (以下を参照ください)
をテストの実行時にアタッチします。
$listener = new MyListener(
array('Sebastian'),
22,
'April',
19.78,
NULL,
new stdClass
);
<php> 要素とその子要素を使って、
PHP の設定や定数、グローバル変数を設定します。また、
include_path の先頭にパスを追加することもできます。
<php> <includePath>.</includePath> <ini name="foo" value="bar"/> <const name="foo" value="bar"/> <var name="foo" value="bar"/> <env name="foo" value="bar"/> <post name="foo" value="bar"/> <get name="foo" value="bar"/> <cookie name="foo" value="bar"/> <server name="foo" value="bar"/> <files name="foo" value="bar"/> <request name="foo" value="bar"/> </php>
上の XML 設定は、次の PHP コードに対応します。
ini_set('foo', 'bar');
define('foo', 'bar');
$GLOBALS['foo'] = 'bar';
$_ENV['foo'] = 'bar';
$_POST['foo'] = 'bar';
$_GET['foo'] = 'bar';
$_COOKIE['foo'] = 'bar';
$_SERVER['foo'] = 'bar';
$_FILES['foo'] = 'bar';
$_REQUEST['foo'] = 'bar';
<selenium> 要素とその子要素である
<browser> を使って、
Selenium RC サーバのリストを設定します。
<selenium>
<browser name="Firefox on Linux"
browser="*firefox /usr/lib/firefox/firefox-bin"
host="my.linux.box"
port="4444"
timeout="30000"/>
</selenium>上の XML 設定は、次の PHP コードに対応します。
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
public static $browsers = array(
array(
'name' => 'Firefox on Linux',
'browser' => '*firefox /usr/lib/firefox/firefox-bin',
'host' => 'my.linux.box',
'port' => 4444,
'timeout' => 30000
)
);
// ...
}Copyright (c) 2005-2012 Sebastian Bergmann.
この作品は、Creative Commons Attribution License の下で
ライセンスされています。このライセンスの内容を確認するには、
http://creativecommons.org/licenses/by/2.0/ を訪問するか、あるいは
Creative Commons, 559 Nathan Abbott Way, Stanford, California 943.6,
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/.
====================================================================