製作著作 © 2005, 2006, 2007, 2008, 2009 Sebastian Bergmann
PHPUnit 3.3 対応版 Updated on 2009-12-29.
assertArrayHasKey()assertClassHasAttribute()assertClassHasStaticAttribute()assertContains()assertContainsOnly()assertEqualXMLStructure()assertEquals()assertFalse()assertFileEquals()assertFileExists()assertGreaterThan()assertGreaterThanOrEqual()assertLessThan()assertLessThanOrEqual()assertNotNull()assertObjectHasAttribute()assertRegExp()assertSame()assertSelectCount()assertSelectEquals()assertSelectRegExp()assertStringEqualsFile()assertTag()assertThat()assertTrue()assertType()assertXmlFileEqualsXmlFile()assertXmlStringEqualsXmlFile()assertXmlStringEqualsXmlString()どんなにすぐれたプログラマも、間違いを犯します。 よいプログラマとそうでないプログラマの違いは、 よいプログラマはテストを行って間違いをできるだけ早く発見してしまうことです。 テストをするのが早ければ早いほど間違いを発見しやすくなり、 またそれを修正しやすくなります。 リリース直前までテストを先延ばしにしておくことが非常に問題であるのはこのためです。 そんなことをすると、すべてのエラーを発見しきることができず、 発見したエラーを修正することも非常に難しくなります。結局は、 トリアージを行ってどのエラーに対応するかを判断しなければならなくなります。 なぜならすべてのエラーを完全に修正することは不可能だからです。
PHPUnit を使用したテストは、全体としてはあなたがこれまでに行ってきたことと同じです。 ただ、そのやり方が違うだけです。それは、テスト つまりあなたのプログラムが期待通りにふるまうことを調べることと 総合テスト つまり実行可能なコード片がソフトウェアの各部分 (部品) を自動的にテストすることとの違いになります。実行可能なコード片のことを、 単体テスト (unit test) と呼びます。
この章では、単純な print
ベースのテストコードをもとにして完全な自動テストに書き換えていきます。
PHP 組み込みの array
をテストするように頼まれたとしましょう。このオブジェクトの機能のひとつに、
関数 sizeof() があります。新しく作成された配列では、
sizeof() 関数は 0 を返すはずです。
そして要素を 1 つ追加すると sizeof() は
1 を返すようになるはずです。テストしたい内容を
例 1.1
に示します。
例 1.1: Array および sizeof() のテスト
<?php
$fixture = array();
// $fixture は空のはずです。
$fixture[] = 'element';
// $fixture はひとつの要素を含むはずです。
?>
期待通りの結果が得られているかどうかを調べるためのいちばん単純な方法は、
要素を追加する前と後に sizeof() の結果を表示することです
(例 1.2 を参照ください)。
それぞれ 0 および 1 が得られたら、
array および sizeof()
が期待通りに動作していることになります。
例 1.2: print を使用した Array および sizeof() のテスト
<?php
$fixture = array();
print sizeof($fixture) . "\n";
$fixture[] = 'element';
print sizeof($fixture) . "\n";
?>
0 1
このテストは、成功したかどうかの判断を (出力結果を見て)
手動で行わなければなりません。今度は、この判断を自動でできるようにしてみましょう。
例 1.3 では、
期待される結果と実際の結果をコード中で比較して、もしそれらの値が等しければ
ok と表示します。もし not ok
と表示された場合は、どこかがおかしいということがわかります。
例 1.3: 期待値と実際の値を比較することによる Array および sizeof() のテスト
<?php
$fixture = array();
print sizeof($fixture) == 0 ? "ok\n" : "not ok\n";
$fixture[] = 'element';
print sizeof($fixture) == 1 ? "ok\n" : "not ok\n";
?>
ok ok
今度は、相違があった際に例外を発生させる関数を用意して、 期待値と実際の値を比較する処理を抽出してみましょう (例 1.4)。 これには 2 つの利点があります。テストが記述しやすくなること、 そして何か問題があったときにのみそれを出力させることができるということです。
例 1.4: アサーション関数を使用した Array および sizeof() のテスト
<?php
$fixture = array();
assertTrue(sizeof($fixture) == 0);
$fixture[] = 'element';
assertTrue(sizeof($fixture) == 1);
function assertTrue($condition)
{
if (!$condition) {
throw new Exception('Assertion failed.');
}
}
?>
これで、テストは完全に自動化されました。最初のバージョンでは単に テストする だけでしたが、このバージョンでは 自動テスト になっています。
自動テストを行う目的は、間違いを少なくすることです。 いくらすばらしいテストを行ったところで あなたのコードが完璧なものになるわけではありませんが、 自動テストを始めることで不具合の量を劇的に減らすことになるでしょう。 自動テストによってあなたのコードは信頼性の高いものとなり、大胆な設計変更 (リファクタリング) を行ったりチームメイトとの関係をよりよくしたり (複数チームでのテスト)、 その日の朝に比べて帰宅前のコードがよりよくなっていることを確信できたりといった効果があります。
いまのところ、組み込みの array および
sizeof() 関数のテストしかありません。
PHP が提供する数多くの array_*()
関数をテストしようとすると、それらそれぞれについてテストを記述する必要があります。
それらのすべてのテストについて基盤部分を最初から書いていくこともできますが、
共通部分は一度だけ記述するようにし、
個々のテストではテスト固有の部分のみを記述していくほうがずっとよい方法です。
PHPUnit は、そのような基盤部分を提供します。
PHPUnit のようなフレームワークには、解決しなければならない制約があります。 その中のいくつかはお互いに相反するものです。 テストは、以下の条件を同時に満たす必要があります。
テストの書き方を身に着けるのが難しければ、 開発者はそんなものを覚えようとしないでしょう。
テストが書きにくければ、開発者はそんなものを書こうとしないでしょう。
テストコードには、外部からの要素を含めるべきではありません。 そうするとテストコードが周りのノイズに埋もれてしまいます。
ボタン一発でテストが実行でき、 その結果は明白な形式で表示されなければなりません。
一日に何百何千というテストを実行できるよう、 テストはすばやく実行できなければなりません。
テストは、お互い他のテストに影響を及ぼしてはいけません。 テストの実行順序を変えることでテストの結果が変わってしまってはいけません。
テストの数や組み合わせを自由に選択できなければなりません。 テストが独立している以上、これは当然のことです。
これらの制約の中には、相反する項目が 2 点あります。
一般に、テストにはプログラミング言語の全機能を必要としません。 多くのテストツールは、テストを記述するため必要最小限の機能のみを組み込んだ 独自のスクリプト言語を提供しています。その結果、テストは読みやすく、 また書きやすいものになります。 なぜならテストの内容から気をそらせるノイズがないためです。しかし、 またひとつ新たなプログラミング言語やプログラミングツールの使い方を覚える必要があり、 不便です。
ひとつのテストが他のテストの結果に何の影響もおよぼさないようにするには、 各テストの実行前に毎回テスト用の環境を構築し、 終了後には毎回それを元の状態に戻す必要があります。しかし、環境を構築する (例: データベースに接続し、特定の状態を表すデータを投入する) には長い時間がかかります。
PHPUnit は、テスト言語として PHP を使用することで これらの衝突を回避しようとしています。小規模で単純なテストを行う際には、 PHP の機能は行き過ぎた面もあるかもしれません。しかし、PHP を使用することで、 これまでの開発経験や開発ツールを武器として利用できます。 あまり気乗りしないテスターを納得させるため、 テストを最初に書き始める際の負担をできるだけ下げることが重要だと考えています。
PHPUnit では、実行速度よりもテストの独立性を重視しています。 独立したテストに価値があるのは、そのほうがより高品質のフィードバックが得られるからです。 一連のテストの最初のほうで失敗したことでその後のすべてのテストが失敗してしまい、 大量の失敗報告を受け取るようなことがなくなります。 このオリエンテーションでは独立したテストを目指し、 シンプルなオブジェクトを数多く作るという設計を心がけます。 各オブジェクトは独立してすばやくテストできます。結果としてよりよい設計 に加えてより高速なテストが可能となります。
PHPUnit ではほとんどのテストが成功することを想定しており、 成功したテストの詳細について報告することはあまり価値がないと考えています。 テストが失敗した場合には、そのことをしっかり報告しなければなりません。 大半のテストは成功すべきであり、 それらについては実行したテストの数以外に特にコメントすべき情報はありません。 これは、PHPUnit のコアではなく報告用のクラス群に組み込まれている機能です。 テストの結果が表示される際には実行したテストの数が表示されますが、 詳細が表示されるのは失敗したテストについてだけです。
テストは決め細やかに行うこと、つまりひとつのテストではひとつのオブジェクトの ひとつの機能についてをテストするようにすることが期待されています。 そのため、最初のテストは失敗し、テストの実行は終了し、 PHPUnit は失敗を報告します。多くの小さなテストを実行させることは一種の芸術です。 決め細やかなテストにより、システム全体の設計がよりよいものとなります。
PHPUnit でオブジェクトをテストする際には、 その公開インターフェースについてのみテストを行います。 公開されている振る舞いにのみ基づいてテストを行うことで、 設計上の困難な問題により早い段階で対応できるようになり、 設計ミスがシステムの大部分に影響を及ぼすことを避けられます。
PHPUnit をインストールするには、 PEAR インストーラ を使用します。このインストーラは PEAR の根幹をなすものであり、 PHP のパッケージを配布する仕組みを提供しています。また、バージョン 4.3.0 以降のすべての PHP に同梱されています。
PHPUnit の配布に使用する PEAR チャネル (pear.phpunit.de)
を、ローカルの PEAR 環境に登録する必要があります。
pear channel-discover pear.phpunit.deこれを行う必要があるのは最初の一度だけです。これで、PEAR インストーラは PHPUnit チャネルからパッケージをインストールできるようになります。
pear install phpunit/PHPUnit
インストールすると、PHPUnit のソースファイルがローカルの PEAR
ディレクトリに格納されます。場所は、通常は
/usr/lib/php/PHPUnit です。
PHPUnit がサポートしているのは PEAR インストーラを使用する方法のみですが、 PHPUnit を手動でインストールすることも可能です。そのためには、 以下の手順に従ってください。
http://pear.phpunit.de/get/
からアーカイブをダウンロードし、それを php.ini
設定ファイルの include_path
で指定したディレクトリに展開します。
phpunit スクリプトを準備します。
phpunit.php スクリプトの名前を
phpunit に変更します。
その中の @php_bin@ という文字列を、
PHP コマンドラインインタプリタへのパス (通常は
/usr/bin/php) に変更します。
それをパスの通ったディレクトリにコピーし、
実行可能属性を付与します (chmod +x phpunit)。
PHPUnit/Util/Fileloader.php スクリプトを準備します。
その中の @php_bin@ という文字列を、
PHP コマンドラインインタプリタへのパス (通常は
/usr/bin/php) に変更します。
例 4.1 は、PHPUnit を使用する形式で 例 1.4 の 2 つのテストを書き直したものです。
例 4.1: PHPUnit を使用した Array および sizeof() のテスト
<?php
require_once 'PHPUnit/Framework.php';
class ArrayTest extends PHPUnit_Framework_TestCase
{
public function testNewArrayIsEmpty()
{
// 配列を作成します。
$fixture = array();
// 配列のサイズは 0 です。
$this->assertEquals(0, sizeof($fixture));
}
public function testArrayContainsAnElement()
{
// 配列を作成します。
$fixture = array();
// 配列にひとつの要素を追加します。
$fixture[] = 'Element';
// 配列のサイズは 1 です。
$this->assertEquals(1, sizeof($fixture));
}
}
?>
Whenever you are tempted to type something into a
何かを | ||
| --Martin Fowler | ||
例 4.1 では、 PHPUnit を使用してテストを記述する基本手順を説明しています。
Class という名前のクラスのテストは、ClassTest という名前のクラスに記述します。
ClassTest は、(ほとんどの場合) PHPUnit_Framework_TestCase を継承します。
テストは、test* という名前のパブリックメソッドとなります。
あるいは、@test アノテーションをメソッドのコメント部で使用することで、それがテストメソッドであることを示すこともできます。
テストメソッドの中で assertEquals() のようなアサーションメソッド (「PHPUnit_Framework_Assert」 を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。
テストメソッドには任意の引数を渡すことができます。
この引数は、データプロバイダメソッド
(例 4.2
の provider())
で指定します。使用するデータプロバイダメソッドを指定するには
@dataProvider アノテーションを使用します。
データプロバイダメソッドは、public
でなければなりません。また、
メソッドの返り値の型は、配列の配列あるいはオブジェクト
(Iterator インターフェイスを実装しており、
反復処理の際に配列を返すもの) である必要があります。
この返り値の各要素に対して、その配列の中身を引数としてテストメソッドがコールされます。
例 4.2: データプロバイダの使用
<?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.3.0 by Sebastian Bergmann.
...F
Time: 0 seconds
There was 1 failure:
1) testAdd(DataTest) with data (1, 1, 3)
Failed asserting that <integer:2> matches expected value <integer:3>.
/home/sb/DataTest.php:21
FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
例 4.3
は、テストするコード内で例外がスローされたかどうかを
@expectedException アノテーションを使用して調べる方法を示すものです。
例 4.3: @expectedException アノテーションの使用法
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase
{
/**
* @expectedException InvalidArgumentException
*/
public function testException()
{
}
}
?>
phpunit ExceptionTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testException(ExceptionTest)
Expected exception InvalidArgumentException
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
一方、setExpectedException()
メソッドを使用して、発生するであろう例外を指定することもできます。この方法を
例 4.4
に示します。
例 4.4: テスト対象のコードで発生するであろう例外の指定
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase
{
public function testException()
{
$this->setExpectedException('InvalidArgumentException');
}
}
?>
phpunit ExceptionTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testException(ExceptionTest)
Expected exception InvalidArgumentException
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.表 4.1 は、例外をテストするために用意されているメソッドをまとめたものです。
表4.1 例外のテスト用のメソッド
| メソッド | 意味 |
|---|---|
void setExpectedException(string $exceptionName) | 発生することを期待する例外の名前を $exceptionName に設定します。 |
String getExpectedException() | 発生することを期待する例外の名前を返します。 |
一方、 例 4.5 のような方法で例外をテストすることもできます。
例 4.5: 例外をテストするための、別の方法
<?php
require_once 'PHPUnit/Framework.php';
class ExceptionTest extends PHPUnit_Framework_TestCase {
public function testException() {
try {
// ... 例外が発生するであろうコード ...
}
catch (InvalidArgumentException $expected) {
return;
}
$this->fail('期待通りの例外が発生しませんでした。');
}
}
?>
例外が発生するはずの
例 4.5
のコードで例外が発生しなかった場合、それに続く
fail() (表 22.2 を参照ください)
によってテストが終了し、問題を報告します。期待通りに例外が発生すると、
catch ブロックが実行されてテストは正常終了します。
デフォルトでは、PHPUnit はテストの実行中に発生した PHP のエラーや警告そして notice を例外に変換します。これらの例外を用いて、たとえば 例 4.6 のように PHP のエラーが発生することをテストできます。
例 4.6: @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.3.13 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 1 assertion)
PHPUnit_Framework_Error_Notice および
PHPUnit_Framework_Error_Warning は、
それぞれ PHP の notice と警告に対応します。
phpunit コマンドを実行すると、PHPUnit
のコマンドライン版テストランナーが起動します。
コマンドラインのテストランナーを使用したテストの様子を以下に示します。
phpunit ArrayTest
PHPUnit 3.3.0 by Sebastian Bergmann.
..
Time: 0 seconds
OK (2 tests, 2 assertions)テストがひとつ実行されるたびに、PHPUnit コマンドラインツールはその経過を示す文字を出力します。
PHPUnit は、失敗 (failures) と
エラー (errors) を区別します。
「失敗」は PHPUnit のアサーションに違反した場合、つまり例えば
assertEquals() のコールに失敗した場合などで、
「エラー」は予期せぬ例外や PHP のエラーが発生した場合となります。
この区別は、時に有用です。というのは「エラー」は一般的に「失敗」
より修正しやすい傾向があるからです。
もし大量の問題が発生した場合は、まず「エラー」を最初に片付け、
その後で「失敗」を修正していくのが最良の方法です。
以下のコードで、コマンドライン版テストランナーのスイッチの一覧を見てみましょう。
phpunit --help
PHPUnit 3.3.0 by Sebastian Bergmann.
Usage: phpunit [switches] UnitTest [UnitTest.php]
phpunit [switches] <directory>
--log-json <file> Log test execution in JSON format.
--log-tap <file> Log test execution in TAP format to file.
--log-xml <file> Log test execution in XML format to file.
--coverage-html <dir> Generate code coverage report in HTML format.
--coverage-clover <file> Write code coverage data in Clover XML format.
--coverage-source <dir> Write code coverage / source data in XML format.
--test-db-dsn <dsn> DSN for the test database.
--test-db-log-rev <rev> Revision information for database logging.
--test-db-prefix ... Prefix that should be stripped from filenames.
--test-db-log-info ... Additional information for database logging.
--story-html <file> Write Story/BDD results in HTML format to file.
--story-text <file> Write Story/BDD results in Text format to file.
--testdox-html <file> Write agile documentation in HTML format to file.
--testdox-text <file> Write agile documentation in Text format to file.
--filter <pattern> Filter which tests 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.
--loader <loader> TestSuiteLoader implementation to use.
--repeat <times> Runs the test(s) repeatedly.
--story Report test execution progress in Story/BDD format.
--tap Report test execution progress in TAP format.
--testdox Report test execution progress in TestDox format.
--colors Use colors in output.
--no-syntax-check Disable syntax check of test source files.
--stop-on-failure Stop execution upon first error or failure.
--verbose Output more verbose information.
--wait Waits for a keystroke after each test.
--skeleton-class Generate Unit class for UnitTest in UnitTest.php.
--skeleton-test Generate UnitTest class for Unit in Unit.php.
--help Prints this usage information.
--version Prints the version and exits.
--bootstrap <file> A "bootstrap" PHP file that is run before the tests.
--configuration <file> Read configuration from XML file.
-d key[=value] Sets a php.ini value.phpunit UnitTest
UnitTest という名前のクラスで定義されている
テストを実行します。このクラスは、UnitTest.php
という名前のファイルの中に定義されているものとします。
UnitTest は、PHPUnit_Framework_TestCase
を継承したクラスであるか、あるいは PHPUnit_Framework_Test
オブジェクト、例えば PHPUnit_Framework_TestSuite
のインスタンスを返す public static suite()
というメソッドを保持するクラスでなければなりません。
phpunit UnitTest UnitTest.php
UnitTest という名前のクラスで定義されているテストを実行します。
このクラスは、指定したファイルの中で定義されているものとします。
--log-json--log-tapTest Anything Protocol (TAP) フォーマットを使用して、テストの実行結果のログを作成します。 詳細は 第 19 章 を参照ください。
--log-xmlテストの実行結果を XML 形式のログファイルに出力します。 詳細は 第 19 章 を参照ください。
--coverage-htmlコードカバレッジレポートを HTML 形式で作成します。 詳細は 第 15 章 を参照ください。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--coverage-clover実行したテストのコードカバレッジ情報を含むログファイルを XML 形式で作成します。詳細は 第 19 章 を参照ください。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--coverage-sourceカバーする PHP ソースファイルごとにひとつずつの XML ファイルを、指定したディレクトリに作成します。 各 <line> 要素には、コードカバレッジ情報がついた PHP ソースコードの行が保持されます。
この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--test-db-*テストの結果やコードカバレッジデータをデータベースに書き込みます。 詳細は 第 19 章 を参照ください。
この機能は、PDO 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。
--story-html および --story-text振舞駆動開発のシナリオとして実行するためのレポートを HTML あるいはプレーンテキスト形式で作成します。詳細は 第 14 章 を参照ください。
--testdox-html および --testdox-text実行したテストについて、HTML あるいはプレーンテキスト形式のドキュメントを生成します 詳細は 第 16 章 を参照ください。
--filter指定したパターンにマッチする名前のテストのみを実行します。 パターンとして指定できるのは、単一のテスト名か、 あるいは複数のテスト名にマッチする 正規表現 です。
--group
指定したグループのテストのみを実行します。
あるテストを特定のグループに所属させるには、
@group アノテーションを使用します。
--exclude-group
指定したグループをテストの対象外とします。
あるテストを特定のグループに所属させるには、
@group アノテーションを使用します。
--list-groups使用可能なテストグループの一覧を表示します。
--loader
PHPUnit_Runner_TestSuiteLoader を実装したクラスのうち、
実際に使用するものを指定します。
標準のテストスイートローダーは、現在の作業ディレクトリおよび PHP
の設定項目 include_path
で指定されているディレクトリからソースファイルを探します。
PEAR の命名規則に従い、Project_Package_Class
クラスがソースファイル Project/Package/Class.php
に対応します。
--repeat指定された回数だけ、繰り返しテストを実行します。
--storyテストの進捗状況レポートを、振舞駆動開発に適した形式で出力します。 詳細は 第 14 章 を参照ください。
--tapTest Anything Protocol (TAP) を使用して、テストの進行状況を報告します。 詳細は 第 19 章 を参照ください。
--testdoxテストの進行状況を、アジャイルな文書として報告します。 詳細は 第 16 章 を参照ください。
--colors出力に色を使用します。
--no-syntax-checkテストソースファイルの構文チェックを無効にします。
--stop-on-failure最初にエラーあるいは失敗が発生した時点で実行を停止します。
--verboseより詳細な情報を出力します。例えば、 未完成のテストや省略したテストの名前が表示されます。
--wait各テストが終了するたびにキー入力待ちの状態になります。これは、 テストランナーが終了すると同時にウィンドウが閉じてしまうような場合に便利です。
--skeleton-class
(UnitTest.php に記述された)
テストケースクラス UnitTest
を元に、雛形クラス Unit を
(Unit.php という名前で) 作成します。
詳細は 第 17 章 を参照ください。
--skeleton-test
(UnitTest.php に記述された) Unit
クラスに対して、テストケースクラス UnitTest
の雛形を記述したファイル UnitTest.php を作成します。
詳細は 第 17 章 を参照ください。
--bootstrapテストの前に実行される "ブートストラップ" PHP ファイルを指定します。
--configuration設定を XML ファイルから読み込みます。 詳細は 付録 B を参照ください。
phpunit.xml
が現在の作業ディレクトリに存在しており、かつ --configuration
が使われていない場合、設定が自動的にそのファイルから読み込まれます。
-d指定した PHP 設定オプションの値を設定します。
テストを記述する際にいちばん時間を食うのは、テストを開始するための事前設定と テスト終了後の後始末の処理を書くことです。この事前設定は、テストの fixture と呼ばれます。
例 4.1
では、fixture は
$fixture という変数に格納された配列だけでした。
しかしたいていの場合は fixture はこれより複雑なものとなり、
それを準備するにはかなりの量のコードが必要です。本来のテストの内容が、
fixture を設定するためのコードの中に埋もれてしまうことになります。
この問題は、複数のテストで同じような fixture を設定する場合により顕著になります。
テストフレームワークの助けがなければ、
個々のテストのなかで同じような準備コードを繰り返し書くはめになってしまいます。
PHPUnit は、準備用のコードの共有をサポートしています。
各テストメソッドが実行される前に、setUp()
という名前のテンプレートメソッドが実行されます。setUp()
は、テスト対象のオブジェクトを生成するような処理に使用します。
テストメソッドの実行が終了すると、それが成功したか否かにかかわらず、
tearDown() という名前の別のテンプレートメソッドが実行されます。
tearDown() では、テスト対象のオブジェクトの後始末などを行います。
それでは、setUp() を使用してコードの重複を排除するように
例 4.1
を書き換えてみましょう。
まず最初にインスタンス変数 $fixture を宣言し、
メソッド内のローカル変数ではなくこちらを使用するようにします。
そして、array fixture の生成処理を
setUp() メソッドに移動します。最後に、
テストメソッド内で重複しているコードを取り除き、
新しく作成したインスタンス変数を使用するようにします。つまり、
assertEquals() で使用しているローカル変数
$fixture を、$this->fixture
に置き換えます。
例 6.1: setUp() を使用して Array fixture を作成する
<?php
require_once 'PHPUnit/Framework.php';
class ArrayTest extends PHPUnit_Framework_TestCase
{
protected $fixture;
protected function setUp()
{
// Array fixture を作成します。
$this->fixture = array();
}
public function testNewArrayIsEmpty()
{
// Array fixture のサイズは 0 のはずです。
$this->assertEquals(0, sizeof($this->fixture));
}
public function testArrayContainsAnElement()
{
// Array fixture に要素を追加します。
$this->fixture[] = 'Element';
// Array fixture のサイズは 1 のはずです。
$this->assertEquals(1, sizeof($this->fixture));
}
}
?>
各テストメソッドが実行されるたびに、setUp()
および tearDown() が一度ずつコールされます。
「テストケース内の全テストメソッドについて一度だけコールするように
したほうがよいのではないか」とお考えになるかもしれませんが、
そのようにすると各テストを互いに独立した状態にすることが難しくなります。
テストメソッドごとに setUp() および
tearDown() が一度ずつ実行されるだけでなく、
テストメソッドごとに、新しいテストケースクラスのインスタンスが作成されます。
setUp() と tearDown()
は理屈上では対称的になるはずですが、実際にはそうではありません。実際には、
tearDown() を実装する必要があるのは setUp()
で外部リソース (ファイルやソケットなど) を割り当てた場合のみです。もし
setUp() で単に PHP オブジェクトを作成しただけの場合は、
一般には tearDown() は必要ありません。しかし、もし
setUp() で大量のオブジェクトを作成した場合には、
それらの後始末をするために tearDown() で変数を
unset() したくなることもあるでしょう。
テストケースオブジェクト自体のガベージコレクションにはあまり意味がありません。
ふたつのテストがあって、それぞれの setup がほんの少しだけ違う場合にはどうなるでしょう? このような場合は、二種類の可能性が考えられます。
もし setUp() の違いがごくわずかなものなら、
その違う部分を setUp()
からテストメソッドのほうに移動させます。
setUp() の違いが大きければ、
テストケースクラスを別に分ける必要があります。それぞれのクラスには、
setup の違いを表す名前をつけます。
複数のテストの間で fixture を を共有する利点は、ほとんどありません。 しかし、設計上の問題などでどうしても fixture を共有しなければならないこともあるでしょう。
複数のテスト間で共有する意味のある fixture の例として意味のあるものといえば、 データベースとの接続でしょう。テストのたびに新しいデータベース接続を毎回作成するのではなく、 最初にログインした状態を再利用するということです。こうすることで、 テストの実行時間を短縮できます。
例 6.2
は、PHPUnit_Framework_TestSuite クラス
(「Using the TestSuite Class」 を参照ください)
のテンプレートメソッド
setUp() および tearDown() を使用します。
これらを使用して、
最初のテストの前にデータベースとの接続を行い、
最後のテストが終わった後にデータベースとの接続を解除します。
PHPUnit_Framework_TestSuite オブジェクトの属性
$sharedFixture が、
PHPUnit_Framework_TestSuite
および PHPUnit_Framework_TestCase
オブジェクトから使用できます。
例 6.2: テストスイートの複数テスト間での fixture の共有
<?php
require_once 'PHPUnit/Framework.php';
class DatabaseTestSuite extends PHPUnit_Framework_TestSuite
{
protected function setUp()
{
$this->sharedFixture = new PDO(
'mysql:host=wopr;dbname=test',
'root',
''
);
}
protected function tearDown()
{
$this->sharedFixture = NULL;
}
}
?>
このように fixture を共有することがテストの価値を下げてしまうということを、 まだうまく伝え切れていないかもしれません。問題なのは、 各オブジェクトが疎結合になっていないという設計なのです。 複数が連携しているようなテストを作って設計上の問題から目をそらしてしまうのではなく、 きちんと設計しなおした上で、スタブ (第 11 章 を参照ください) を使用するテストを書くことをお勧めします。
singleton を使用するコードをテストするのはたいへんです。 同様に、グローバル変数を使うコードのテストもまたたいへんです。 一般に、テスト対象のコードはグローバル変数と密接に関連しており、 グローバル変数の内容を制御することはできません。 さらに別の問題もあって、あるテストの中でグローバル変数を変更してしまうと 別のテストがうまく動かなくなる可能性があります。
PHP では、グローバル変数は次のような動きをします。
グローバル変数 $foo = 'bar'; は、$GLOBALS['foo'] = 'bar'; として格納される。
$GLOBALS はスーパーグローバル変数と呼ばれる。
スーパーグローバル変数は組み込みの変数で、すべてのスコープで常に利用できる。
関数やメソッドのスコープでグローバル変数 $foo にアクセスするには、直接 $GLOBALS['foo'] にアクセスするか、あるいは global $foo; を用いて (グローバル変数を参照する) ローカル変数を作成する。
デフォルトでは、PHPUnit がテストを実行する際には、
グローバル変数やスーパーグローバル変数 ($GLOBALS,
$_ENV, $_POST,
$_GET, $_COOKIE,
$_SERVER, $_FILES,
$_REQUEST)
への変更が他のテストへの影響を及ぼさないようにします。
グローバル変数やスーパーグローバル変数のバックアップ・リストアの実装には
serialize() および
unserialize() を使用しています。
PHP 組み込みの一部のクラス、たとえば PDO
のオブジェクトはシリアライズできないため、そのようなオブジェクトが
$GLOBALS 配列に格納されている場合はバックアップ操作が失敗します。
グローバル変数およびスーパーグローバル変数のバックアップ・リストアを テストケースクラス内のすべてのテストで完全に無効化するには、このようにします。
class MyTest extends PHPUnit_Framework_TestCase
{
protected $backupGlobals = FALSE;
// ...
}
$backupGlobals
をたとえば setUp()
メソッド内で設定しても効果が及ばないことに注意しましょう。
PHPUnit の目指すところ (第 2 章 を参照ください) のひとつに 「自由に組み合わせられる」ということがあります。つまり、 例えば「そのプロジェクトのすべてのテストを実行する」「プロジェクトの中の ある部品を構成するすべてのクラスについて、すべてのテストを実行する」 「特定のひとつのクラスのテストのみを実行する」など、 数や組み合わせにとらわれずに好きなテストを一緒に実行できるということです。
PHPUnit supports different ways of organizing tests and composing them into a test suite. This chapter shows the most commonly used approaches.
Probably the easiest way to compose a test suite is to keep all test case source files in a test directory. PHPUnit can automatically discover and run the tests by recursively traversing the test directory.
Lets take a look at the test suite of the Object_Freezer
library. Looking at this project's directory structure, we see that the
test case classes in the Tests directory mirror the
package and class structure of the System Under Test (SUT) in the
Object directory:
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
To run all tests for the library we just need to point the PHPUnit command-line test runner to the test directory:
phpunit Tests
PHPUnit 3.4.0 by Sebastian Bergmann.
............................................................ 60 / 75
...............
Time: 0 seconds
OK (75 tests, 164 assertions)
To run only the tests that are declared in the Object_FreezerTest
test case class in Tests/FreezerTest.php we can use
the following command:
phpunit Tests/FreezerTest
PHPUnit 3.4.0 by Sebastian Bergmann.
............................
Time: 0 seconds
OK (28 tests, 60 assertions)
For more fine-grained control of which tests to run we can use the
--filter switch:
phpunit --filter testFreezingAnObjectWorks Tests
PHPUnit 3.4.0 by Sebastian Bergmann.
.
Time: 0 seconds
OK (1 test, 2 assertions)A drawback of this approach is that we have no control over the order in which the test are run.
PHPUnit's XML configuration file (付録 B)
can also be used to compose a test suite.
例 7.1
shows a minimal example that will add all *Test classes
that are found in *Test.php files when the
Tests is recursively traversed.
例 7.1: Composing a Test Suite Using XML Configuration
<phpunit
<testsuite name="Object_Freezer">
<directory>Tests</directory>
</testsuite>
</phpunit>A drawback of this approach is that we have no control over the order in which the test are run.
Alternatively, we can make the order in which tests are executed explicit:
例 7.2: Composing a Test Suite Using XML Configuration
<phpunit
<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>
</phpunit>
The PHPUnit_Framework_TestSuite class of the PHPUnit
framework allows us to organize tests into a hierarchy of test suite
objects.
例 7.3 shows
the top-level AllTests class for a project that has a
package named Package.
例 7.3: The top-level AllTests class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Package/AllTests.php';
// ...
class AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('Project');
$suite->addTest(Package_AllTests::suite());
// ...
return $suite;
}
}
?>
The top-level AllTests class aggregates the
package-level Package_AllTests class that in turn
aggregates the test case classes for the classes of said package.
例 7.4: The Package_AllTests class
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Framework/ClassTest.php';
// ...
class Package_AllTests
{
public static function suite()
{
$suite = new PHPUnit_Framework_TestSuite('Package');
$suite->addTestSuite('Package_ClassTest');
// ...
return $suite;
}
}
?>
Package_ClassTest クラスは、
基底クラス PHPUnit_Framework_TestCase
を継承した標準的なテストケースクラスです。
phpunit AllTests を
Tests ディレクトリで実行すると、すべてのテストを実行します。
phpunit AllTests を
Tests/Package ディレクトリで実行すると、
Package_* クラスのテストのみを実行します。
phpunit ClassTest を
Tests/Package ディレクトリで実行すると、
Package_Class クラスのテストのみを実行します
(これは Package_ClassTest クラスで宣言されています)。
phpunit --filter testSomething ClassTest を
Tests/Package ディレクトリで実行すると、
Package_ClassTest クラスの
testSomething という名前のテストのみを実行します。
The PHPUnit_Framework_TestSuite class offers two
template methods, setUp() and tearDown(),
that are called before the first test of the test suite and after the last
test of the test suite, respectively.
例 7.5: The MySuite class
<?php
require_once 'MyTest.php';
class MySuite extends PHPUnit_Framework_TestSuite
{
public static function suite()
{
return new MySuite('MyTest');
}
protected function setUp()
{
print __METHOD__ . "\n";
}
protected function tearDown()
{
print __METHOD__ . "\n";
}
}
?>
The MyTest test case class that is added to the test
suite MySuite in
例 7.5
has two test methods, testOne() and testTwo()
as well as the setUp() and tearDown()
methods. The output shows in which order these eight methods are called:
MySuite::setUp() MyTest::setUp() MyTest::testOne() MyTest::tearDown() MyTest::setUp() MyTest::testTwo() MyTest::tearDown() MySuite::tearDown()
Variables stored in $this->sharedFixture by the
setUp() method of the PHPUnit_Framework_TestSuite
class are available as $this->sharedFixture in all the
test that are aggregated by the test suite object (see
「Fixture の共有」).
A TestSuite's setUp() and
tearDown() methods will be called even if no test of
the test suite is run because it is, for instance, filtered.
PHPUnit では、テストクラスの基底クラスである
PHPUnit_Framework_TestCase を拡張するための方法を提供しています。
これにより、出力内容やパフォーマンス低下のテストができるようになります。
メソッドの実行結果を確かめる方法として、(echo や
print などによる)
出力が期待通りのものかを調べたいこともあるでしょう。
PHPUnit_Extensions_OutputTestCase クラスは、PHP の
出力バッファリング 機能を使用してこの仕組みを提供します。
例 8.1
では、PHPUnit_Extensions_OutputTestCase
のサブクラスを作成し、期待する出力内容を expectOutputString()
メソッドで設定する方法を示します。
期待通りの出力が得られなかった場合は、そのテストは失敗という扱いになります。
例 8.1: PHPUnit_Extensions_OutputTestCase の使用法
<?php
require_once 'PHPUnit/Extensions/OutputTestCase.php';
class OutputTest extends PHPUnit_Extensions_OutputTestCase
{
public function testExpectFooActualFoo()
{
$this->expectOutputString('foo');
print 'foo';
}
public function testExpectBarActualBaz()
{
$this->expectOutputString('bar');
print 'baz';
}
}
?>
phpunit OutputTest
PHPUnit 3.3.0 by Sebastian Bergmann.
.F
Time: 0 seconds
There was 1 failure:
1) testExpectBarActualBaz(OutputTest)
Failed asserting that two strings are equal.
expected string <bar>
difference < x>
got string <baz>
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.
表 8.1
PHPUnit_Extensions_OutputTestCase
が提供するメソッドをまとめたものです。
表8.1 OutputTestCase
| メソッド | 意味 |
|---|---|
void expectOutputRegex(string $regularExpression) | 出力が正規表現 $regularExpression にマッチするであろうという予測を設定します。 |
void expectOutputString(string $expectedString) | 出力が文字列 $expectedString と等しくなるであろうという予測を設定します。 |
bool setOutputCallback(callable $callback) | たとえば出力時の正規化などに使用するコールバック関数を設定します。 |
PHPUnit_Extensions_PerformanceTestCase
を継承したテストクラスを使用すると、
関数やメソッドの実行が制限時間内に終わったかどうかなどをテストすることができます。
PHPUnit_Extensions_PerformanceTestCase
のサブクラスを作成してその setMaxRunningTime()
メソッドを使用し、実行時間の最大値を制限する方法を
例 8.2
で示します。
もしテストが制限時間内に終了しなければ、そのテストは失敗という扱いになります。
例 8.2: PHPUnit_Extensions_PerformanceTestCase の使用法
<?php
require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
class PerformanceTest extends PHPUnit_Extensions_PerformanceTestCase
{
public function testPerformance()
{
$this->setMaxRunningTime(2);
sleep(1);
}
}
?>
表 8.2
は、PHPUnit_Extensions_PerformanceTestCase
が実装しているメソッドをまとめたものです。
表8.2 PerformanceTestCase
| メソッド | 意味 |
|---|---|
void setMaxRunningTime(int $maxRunningTime) | テストの所要時間の最大値を (秒単位で) $maxRunningTime に設定します。 |
integer getMaxRunningTime() | このテストの最大所要時間を返します。 |
PHPUnit_Framework_TestCase には、これら以外にも
PHPUnit_Extensions_Database_TestCase や
PHPUnit_Extensions_SeleniumTestCase といった拡張があります。
これらについては、それぞれ 第 9 章
と 第 18 章 で説明します。
作成中のソフトウェアのテストを書いているうちに、 データベースに関するコードをテストする必要が出てくることもあるでしょう。 データベース拡張を使用すると、 データベースを特定の状態にしてからデータベース関連のコードを実行し、 データベースのデータが期待通りになっているかどうかを確かめることができます。
データベース関連のユニットテストを作成するもっとも手っ取り早い方法は、
PHPUnit_Extensions_Database_TestCase クラスを継承することです。
このクラスには、データベース接続を作成したり
データベースにデータを送信したり、
テストの実行後にデータベースの中身を様々な形式のデータセットと比較したりする機能があります。
例 9.1
に、getConnection() と getDataSet()
の実装例を示します。
例 9.1: データベーステストケースの準備
<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getConnection()
{
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
return $this->createDefaultDBConnection($pdo, 'testdb');
}
protected function getDataSet()
{
return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');
}
}
?>
getConnection() メソッドは、
PHPUnit_Extensions_Database_DB_IDatabaseConnection
インターフェイスの実装を返す必要があります。
createDefaultDBConnection()
メソッドを使用して、データベース接続を返すことができます。
このメソッドの最初のパラメータには PDO
オブジェクトを渡し、2 番目のパラメータにはテスト対象のスキーマの名前を渡します。
getDataSet() メソッドは、
PHPUnit_Extensions_Database_DataSet_IDataSet
インターフェイスの実装を返す必要があります。
現在、PHPUnit では 3 種類のデータセットが使用できます。
データセットについては 「データセット」
で説明します。
表9.1 データベーステストケースのメソッド
| メソッド | 意味 |
|---|---|
PHPUnit_Extensions_Database_DB_IDatabaseConnection getConnection() | データベース接続を返すように実装します。これを用いて、期待するデータセットやテーブルを調べます。 |
PHPUnit_Extensions_Database_DataSet_IDataSet getDataSet() | データセットを返すように実装します。データベースの初期設定 (setup) や後始末 (teardown) の際にこれを使用します。 |
PHPUnit_Extensions_Database_Operation_DatabaseOperation getSetUpOperation() | オーバーライドして、各テストの最初に実行する特定の操作を返すようにします。操作の詳細については 「操作」 を参照ください。 |
PHPUnit_Extensions_Database_Operation_DatabaseOperation getTearDownOperation() | オーバーライドして、各テストの最後に実行する特定の操作を返すようにします。操作の詳細については 「操作」 を参照ください。 |
PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection createDefaultDBConnection(PDO $connection, string $schema) | $connection PDO オブジェクトのデータベース接続ラッパーを返します。テスト対象のデータベーススキーマを $schema で指定します。このメソッドの結果を getConnection() の返り値として使用することができます。 |
PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet createFlatXMLDataSet(string $xmlFile) | $xmlFile で指定した絶対パスにある XML ファイルから作成したフラット XML データセットを返します。XML ファイルについての詳細は 「Flat XML データセット」 を参照ください。このメソッドの結果を getDataSet() の返り値として使用することができます。 |
PHPUnit_Extensions_Database_DataSet_XmlDataSet createXMLDataSet(string $xmlFile) | $xmlFile で指定した絶対パスにある XML ファイルから作成した XML データセットを返します。XML ファイルについての詳細は 「XML データセット」 を参照ください。このメソッドの結果を getDataSet() の返り値として使用することができます。 |
void assertTablesEqual(PHPUnit_Extensions_Database_DataSet_ITable $expected, PHPUnit_Extensions_Database_DataSet_ITable $actual) | $expected テーブルの内容が $actual テーブルの内容と一致しないときにエラーを報告します。 |
void assertDataSetsEqual(PHPUnit_Extensions_Database_DataSet_IDataSet $expected, PHPUnit_Extensions_Database_DataSet_IDataSet $actual) | $expected データセットの内容が $actual データセットの内容と一致しないときにエラーを報告します。 |
Data sets are the basic building blocks for both your database fixture as
well as the assertions you may make at the end of your test.
When
returning a data set as a fixture from the PHPUnit_Extensions_Database_TestCase::getDataSet()
method, the default implementation in PHPUnit will automatically truncate
all tables specified and then insert the data from your data set in the
order specified by the data set.
For your convenience there are several
different types of data sets that can be used at your convenience.
The flat XML data set is a very simple XML format that uses a single XML element for each row in your data set. An example of a flat XML data set is shown in 例 9.2.
例 9.2: Flat XML データセット
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<post
post_id="1"
title="My First Post"
date_created="2008-12-01 12:30:29"
contents="This is my first post" rating="5"
/>
<post
post_id="2"
title="My Second Post"
date_created="2008-12-04 15:35:25"
contents="This is my second post"
/>
<post
post_id="3"
title="My Third Post"
date_created="2008-12-09 03:17:05"
contents="This is my third post"
rating="3"
/>
<post_comment
post_comment_id="2"
post_id="2"
author="Frank"
content="That is because this is simply an example."
url="http://phpun.it/"
/>
<post_comment
post_comment_id="1"
post_id="2"
author="Tom"
content="While the topic seemed interesting the content was lacking."
/>
<current_visitors />
</dataset>
As you can see the formatting is extremely simple. Each of the elements
within the root <dataset> element represents a
row in the test database with the exception of the last
<current_visitors /> element (this will be
discussed shortly.) The name of the element is the equivalent of a
table name in your database. The name of each attribute is the
equivalent of a column name in your databases. The value of each
attribute is the equivalent of the value of that column in that row.
This format, while simple, does have some special considerations. The
first of these is how you deal with empty tables. With the default
operation of CLEAN_INSERT you can specify that you
want to truncate a table and not insert any values by listing that
table as an element without any attributes. This can be seen in
例 9.2 with the
<current_visitors /> element. The most common
reason you would want to ensure an empty table as a part of your
fixture is when your test should be inserting data to that table. The
less data your database has, the quicker your tests will run. So if I
where testing my simple blogging software's ability to add comments to
a post, I would likely change
例 9.2 to specify
post_comment as an empty table. When dealing with
assertions it is often useful to ensure that a table is not being
unexpectedly written to, which could also be accomplished in FlatXML
using the empty table format.
The second consideration is how NULL values are
defined. The nature of the flat XML format only allows you to
explicitly specify strings for column values. Of course your database
will convert a string representation of a number or date into the
appropriate data type. However, there are no string representations of
NULL. You can imply a NULL value
by leaving a column out of your element's attribute list. This will
cause a NULL value to be inserted into the database for that column.
This leads me right into my next consideration that makes implicit
NULL values somewhat difficult to deal with.
The third consideration is how columns are defined. The column list for
a given table is determined by the attributes in the first element for
any given table.
In 例 9.2 the
post table would be considered to have the columns
post_id, title,
date_created, contents and
rating. If the first <post>
were removed then the post would no longer be
considered to have the rating column. This means that the first element
of a given name defines the structure of that table. In the simplest of
examples, this means that your first defined row must have a value for
every column that you expect to have values for in the rest of rows for
that table. If an element further into your data set specifies a column
that was not specified in the first element then that value will be
ignored. You can see how this influenced the order of elements in my
dataset in 例 9.2.
I had to specify the second <post_comment>
element first due to the non-NULL value in the url
column.
There is a way to work around the inability to explicitly set
NULL in a flat XML data using the Replacement data
set type. This will be discussed further in
「データセットの交換」.
While the flat XML data set is simple it also proves to be limiting. A more powerful xml alternative is the XML data set. It is a more structured xml format that allows you to be much more explicit with your data set. An example of the XML data set equivalent to the previous flat XML example is shown in 例 9.3.
例 9.3: XML データセット
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<table name="post">
<column>post_id</column>
<column>title</column>
<column>date_created</column>
<column>contents</column>
<column>rating</column>
<row>
<value>1</value>
<value>My First Post</value>
<value>2008-12-01 12:30:29</value>
<value>This is my first post</value>
<value>5</value>
</row>
<row>
<value>2</value>
<value>My Second Post</value>
<value>2008-12-04 15:35:25</value>
<value>This is my second post</value>
<null />
</row>
<row>
<value>3</value>
<value>My Third Post</value>
<value>2008-12-09 03:17:05</value>
<value>This is my third post</value>
<value>3</value>
</row>
</table>
<table name="post_comment">
<column>post_comment_id</column>
<column>post_id</column>
<column>author</column>
<column>content</column>
<column>url</column>
<row>
<value>1</value>
<value>2</value>
<value>Tom</value>
<value>While the topic seemed interesting the content was lacking.</value>
<null />
</row>
<row>
<value>2</value>
<value>2</value>
<value>Frank</value>
<value>That is because this is simply an example.</value>
<value>http://phpun.it</value>
</row>
</table>
<table name="current_visitors">
<column>current_visitors_id</column>
<column>ip</column>
</table>
</dataset>
The formatting is more verbose than that of the Flat XML data set but
in some ways much easier to understand. The root element is again the
<dataset> element. Then directly under that
element you will have one or more <table>
elements. Each <table> element must have a
name attribute with a value equivalent to the name
of the table being represented. The <table>
element will then include one or more <column>
elements containing the name of a column in that table. The
<table> element will also include any number
(including zero) of <row> elements. The
<row> element will be what ultimately
specifies the data to store/remove/update in the database or to check
the current database against. The <row>
element must have the same number of children as there are
<column> elements in that
<table> element. The order of your child
elements is also determined by the order of your
<column> elements for that table. There are
two different elements that can be used as children of the
<row> element. You may use the
<value> element to specify the value of that
column in much the same way you can use attributes in the flat XML data
set. The content of the <value> element will
be considered the content of that column for that row. You may also use
the <null> element to explicitly indicate
that the column is to be given a NULL value. The
<null> element does not contain any attributes
or children.
You can use the DTD in 例 9.4 to validate your XML data set files. A reference of the valid elements in an XML data set can be found in 表 9.2
例 9.4: The XML Data Set DTD
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT dataset (table+) | ANY>
<!ELEMENT table (column*, row*)>
<!ATTLIST table
name CDATA #REQUIRED
>
<!ELEMENT column (#PCDATA)>
<!ELEMENT row (value | null | none)*>
<!ELEMENT value (#PCDATA)>
<!ELEMENT null EMPTY>
表9.2 XML Data Set Element Description
| Element | Purpose | Contents | Attributes |
|---|---|---|---|
<dataset> | The root element of the xml data set file. | One or more <table> elements. | None |
<table> | Defines a table in the dataset. | One or more <column> elements and zero or more <row> elements. | name - The name of the table. |
<column> | Defines a column in the current table. | A text node containing the name of the column. | None |
<row> | Defines a row in the table. | One or more <value> or <null> elements. | None |
<value> | Sets the value of the column in the same position as this value. | A text node containing the value of the corresponding column. | None |
<null> | Sets the value of the column in the same position of this value to NULL. | None | None |
While XML data sets provide a convenient way to structure your data, in many cases they can be time consuming to hand edit as there are not very many XML editors that provide an easy way to edit tabular data via xml. In these cases you may find the CSV data set to be much more useful. The CSV data set is very simple to understand. Each CSV file represents a table. The first row in the CSV file must contain the column names and all subsequent rows will contain the data for those columns.
To construct a CSV data set you must instantiate the PHPUnit_Extensions_Database_DataSet_CsvDataSet class. The constructor takes three parameters: $delimiter, $enclosure and $escape. These parameters allow you to specify the exact formatting of rows within your CSV file. So this of course means it doesn't have to really be a CSV file. You can also provide a tab delimited file. The default constructor will specify a comma delimited file with fields enclosed by a double quote. If there is a double quote within a value it will be escaped by an additional double quote. This is as close to an accepted standard for CSV as there is.
Once your CSV data set class is instantiated you can use the method addTable() to add CSV files as tables to your data set. The addTable method takes two parameters. The first is $tableName and contains the name of the table you are adding. The second is $csvFile and contains the path to the CSV you will be using to set the data for that table. You can call addTable() for each table you would like to add to your data set.
In 例 9.5 you can see an example of how three CSV files can be combined into the database fixture for a database test case.
例 9.5: CSV Data Set Example
--- fixture/post.csv ---
post_id,title,date_created,contents,rating
1,My First Post,2008-12-01 12:30:29,This is my first post,5
2,My Second Post,2008-12-04 15:35:25,This is my second post,
3,My Third Post,2008-12-09 03:17:05,This is my third post,3
--- fixture/post_comment.csv ---
post_comment_id,post_id,author,content,url
1,2,Tom,While the topic seemed interesting the content was lacking.,
2,2,Frank,That is because this is simply an example.,http://phpun.it
--- fixture/current_visitors.csv ---
current_visitors_id,ip
--- DatabaseTest.php ---
<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
require_once 'PHPUnit/Extensions/Database/DataSet/CsvDataSet.php';
class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getConnection()
{
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
return $this->createDefaultDBConnection($pdo, 'testdb');
}
protected function getDataSet()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
$dataSet->addTable('post', 'fixture/post.csv');
$dataSet->addTable('post_comment', 'fixture/post_comment.csv');
$dataSet->addTable('current_visitors', 'fixture/current_visitors.csv');
return $dataSet;
}
}
?>
Unfortunately, while the CSV dataset is appealing from the aspect of edit-ability, it has the same problems with NULL values as the flat XML dataset. There is no native way to explicitly specify a null value. If you do not specify a value (as I have done with some of the fields above) then the value will actually be set to that data type's equivalent of an empty string. Which in most cases will not be what you want. I will address this shortcoming of both the CSV and the flat XML data sets shortly.
新しいテストケースクラスを作成する際には、 これから書くべきテストの内容をはっきりさせるために、 まず最初は以下のような空のテストメソッドを書きたくなることでしょう。
public function testSomething()
{
}
しかし、PHPUnit フレームワークでは空のメソッドを「成功した」
と判断してしまうという問題があります。このような解釈ミスがあると、
テスト結果のレポートが無意味になってしまいます。
そのテストがほんとうに成功したのか、
それともまだテストが実装されていないのかが判断できないからです。
実装していないテストメソッドの中で $this->fail()
をコールするようにしたところで事態は何も変わりません。
こうすると、テストが「失敗した」と判断されてしまいます。
これは未実装のテストが「成功」と判断されてしまうのと同じくらいまずいことです
(訳注: レポートを見ても、そのテストがほんとうに失敗したのか、
まだ実装されていないだけなのかがわかりません)。
テストの成功を青信号、失敗を赤信号と考えるなら、
テストが未完成あるいは未実装であることを表すための黄信号が必要です。
そのような場合に使用するインターフェイスが
PHPUnit_Framework_IncompleteTest で、
これは未完成あるいは未実装のテストメソッドで発生する例外を表すものです。
このインターフェイスの標準的な実装が
PHPUnit_Framework_IncompleteTestError です。
例 10.1
では SampleTest というテストケースクラスを定義しています。
便利なメソッド markTestIncomplete()
(これは、自動的に PHPUnit_Framework_IncompleteTestError
を発生させます) をテストメソッド内でコールすることで、
このメソッドがまだ完成していないことをはっきりさせます。
例 10.1: テストに未完成の印をつける
<?php
require_once 'PHPUnit/Framework.php';
class SampleTest extends PHPUnit_Framework_TestCase
{
public function testSomething()
{
// オプション: お望みなら、ここで何かのテストをしてください。
$this->assertTrue(TRUE, 'This should already work.');
// ここで処理を止め、テストが未完成であるという印をつけます。
$this->markTestIncomplete(
'このテストは、まだ実装されていません。'
);
}
}
?>
未完成のテストは、PHPUnit のコマンドライン版テストランナーでは以下のように
I で表されます。
phpunit --verbose SampleTest
PHPUnit 3.3.0 by Sebastian Bergmann.
SampleTest
I
Time: 0 seconds
There was 1 incomplete test:
1) testSomething(SampleTest)
This test has not been implemented yet.
/home/sb/SampleTest.php:14
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Incomplete: 1.表 10.1 に、テストを未完成扱いにするための API を示します。
表10.1 未完成のテスト用の API
| メソッド | 意味 |
|---|---|
void markTestIncomplete() | 現在のテストを未完成扱いにします。 |
void markTestIncomplete(string $message) | 現在のテストを未完成扱いにします。それを説明する文字列として $message を使用します。 |
すべてのテストがあらゆる環境で実行できるわけではありません。 考えてみましょう。たとえば、データベースの抽象化レイヤーを使用しており、 それがさまざまなドライバを使用してさまざまなデータベースシステムを サポートしているとします。MySQL ドライバのテストができるのは、 当然 MySQL サーバが使用できる環境だけです。
例 10.2
に示すテストケースクラス DatabaseTest には、
テストメソッド testConnection() が含まれています。
このクラスのテンプレートメソッド setUp() では、
MySQLi 拡張モジュールが使用可能かを調べたうえで、もし使用できない場合は
markTestSkipped() メソッドでテストを省略するようにしています。
例 10.2: テストを省略する
<?php
require_once 'PHPUnit/Framework.php';
class DatabaseTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!extension_loaded('mysqli')) {
$this->markTestSkipped(
'MySQLi 拡張モジュールが使用できません。'
);
}
}
public function testConnection()
{
// ...
}
}
?>
飛ばされたテストは、PHPUnit のコマンドライン版テストランナーでは以下のように
S で表されます。
phpunit --verbose DatabaseTest
PHPUnit 3.3.0 by Sebastian Bergmann.
DatabaseTest
S
Time: 0 seconds
There was 1 skipped test:
1) testConnection(DatabaseTest)
The MySQLi extension is not available.
/home/sb/DatabaseTest.php:11
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Skipped: 1.表 10.2 に、テストを省略するための API を示します。
表10.2 テストを省略するための API
| メソッド | 意味 |
|---|---|
void markTestSkipped() | 現在のテストを省略扱いにします。 |
void markTestSkipped(string $message) | 現在のテストを省略扱いにします。それを説明する文字列として $message を使用します。 |
Gerard Meszaros は、Test Doubles の概念を [Meszaros2007] でこのように述べています。
スタブ を用いて SUT が依存している実際のコンポーネントを置き換え、 SUT の入力を間接的にコントロールできるようにすることができます。 これにより、SUT が他の何者も実行しないことを強制させることができます。
例 11.1
に、スタブメソッドの作成と返り値の設定の方法を示します。まず、
PHPUnit_Framework_TestCase クラス (表 22.6 を参照ください) の
getMock() メソッドを用いて
SomeClass オブジェクトのスタブを作成します。
次に、PHPUnit が提供する、いわゆる
Fluent Interface
(流れるようなインターフェイス)
を用いてスタブの振る舞いを指定します。簡単に言うと、
いくつもの一時オブジェクトを作成して、
それらを連結するといった操作は必要ないということです。
そのかわりに、例にあるようにメソッドの呼び出しを連結します。
このほうが、より読みやすく "流れるような" コードとなります。
例 11.1: メソッドに固定値を返させるスタブ
<?php
require_once 'PHPUnit/Framework.php';
class SomeClass
{
public function doSomething()
{
// なにかをします
}
}
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());
}
}
?>
時には、メソッドをコールした際の引数のひとつを
(そのまま) スタブメソッドコールの返り値としたいこともあるでしょう。
例 11.2 は、
returnValue() のかわりに
returnArgument() を用いてこれを実現する例です。
例 11.2: メソッドに引数のひとつを返させるスタブ
<?php
class SomeClass
{
public function doSomething($argument)
{
// なにかをします
}
}
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'));
}
}
?>
スタブメソッドをコールした結果として固定値
(returnValue() を参照ください) や (不変の) 引数
(returnArgument() を参照ください)
ではなく計算した値を返したい場合は、
returnCallback() を使用します。
これは、スタブメソッドからコールバック関数やメソッドの結果を返させます。
例 11.3
を参照ください。
例 11.3: メソッドにコールバックからの値を返させるスタブ
<?php
class SomeClass
{
public function doSomething($argument)
{
// なにかをします
}
}
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'));
}
}
?>
値を返すのではなく、スタブメソッドで例外を発生させることもできます。
例 11.4
に、throwException() でこれを行う方法を示します。
例 11.4: メソッドに例外をスローさせるスタブ
<?php
class SomeClass
{
public function doSomething()
{
// なにかをします
}
}
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())
が正しくコールされたかどうかを調べるものとします。
例 11.5 では、まず
PHPUnit_Framework_TestCase クラスの
getMock() メソッド (表 22.6 を参照ください)
を使用して Observer のモックオブジェクトを作成します。
getMock() メソッドの二番目の (オプションの)
パラメータに配列を指定しているので、Observer
クラスの中の update() メソッドについてのみモック実装が作成されます。
例 11.5: あるメソッドが、指定したパラメータで一度だけコールされることを確かめるテスト
<?php
require_once 'PHPUnit/Framework.php';
class Subject
{
protected $observers = array();
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// なにかをします
// ...
// なにかしたということをオブザーバに通知します
$this->notify('something');
}
protected function notify($argument)
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
}
class Observer
{
public function update($argument)
{
// なにかをします
}
}
class ObserverTest extends PHPUnit_Framework_TestCase
{
public function testUpdateIsCalledOnce()
{
// Observer クラスのモックを作成します。
$observer = $this->getMock('Observer');
// 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();
}
}
?>
表 22.1 は制約の一覧、そして 表 11.1 は使用できる matcher の一覧です。
表11.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 回目に実行された際にマッチするオブジェクトを返します。 |
vfsStream is a stream wrapper for a virtual filesystem that may be helpful in unit tests to mock the real filesystem.
To install vfsStream, the PEAR channel
(pear.php-tools.net) that is used for
its distribution needs to be registered with the local PEAR environment:
pear channel-discover pear.php-tools.netThis has to be done only once. Now the PEAR Installer can be used to install vfsStream:
pear install pat/vfsStream-alpha例 11.6 shows a class that interacts with the filesystem.
例 11.6: A class that interacts with the filesystem
<?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);
}
}
}?>
Without a virtual filesystem such as vfsStream we cannot test the
setDirectory() method in isolation from external
influence (see 例 11.7).
例 11.7: Testing a class that interacts with the filesystem
<?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');
}
}
}
?>
The approach above has several drawbacks:
As with any external resource, there might be intermittent problems with the filesystem. This makes tests interacting with it flaky.
In the setUp() and tearDown() methods we have to ensure that the directory does not exist before and after the test.
When the test execution terminates before the tearDown() method is invoked the directory will stay in the filesystem.
例 11.8 shows how vfsStream can be used to mock the filesystem in a test for a class that interacts with the filesystem.
例 11.8: Mocking the filesystem in a test for a class that interacts with the filesystem
<?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'));
}
}
?>
This has several advantages:
The test itself is more concise.
vfsStream gives the test developer full control over what the filesystem environment looks like to the tested code.
Since the filesystem operations do not operate on the real filesystem anymore, cleanup operations in a tearDown() method are no longer required.
You can always write more tests. However, you will quickly find that only a fraction of the tests you can imagine are actually useful. What you want is to write tests that fail even though you think they should work, or tests that succeed even though you think they should fail. Another way to think of it is in cost/benefit terms. You want to write tests that will pay you back with information. テストはいくらでも書くことができる。でも、じきにわかるだろうが、 きみが考えているテストの中で本当に有用なものはごくわずかだ。 本当に書かなきゃいけないのは、 これは動くだろうと考えているにもかかわらず失敗するテスト。それから、 これは失敗するだろうと考えているにもかかわらず実際は成功するテストだ。 あるいはコストと利益の観点から考えてみてもいいだろう。 きみに何らかの情報を返してくれるテストを書かないとね。 | ||
| --Erich Gamma | ||
開発中のソフトウェアの内部構造を変更し、 わかりやすく変更が簡単なものにする必要が出てきたときのことを考えましょう。 それによってソフトウェアの外部的な振る舞いが変わってしまってはいけません。 この、いわゆる リファクタリング (日本語) を安全に行うにあたり、テストスイートが非常に重要となります。 もしテストスイートがなければ、リファクタリングによってシステムを壊してしまっても あなたはそれに気づかないでしょう。
以下の条件が、あなたのプロジェクトのコードや設計を改善するための助けとなるでしょう。 また、単体テストを使用することで、リファクタリングによって振る舞いが変化していないこと・ エラーが発生していないことが確認できます。
すべての単体テストが正常に動作すること。
コードが設計指針を満たしていること。
コードに冗長性がないこと。
コードには最小限のクラスおよびメソッドのみが含まれていること。
システムに新しい機能を追加する際には、まず最初にテストを書きます。 そのテストがきちんと実行できるようになった時点で、開発は終了です。 この手法については、次の章で詳しく説明します。
不具合の報告を受けたら、すぐにでもそれを修正したいと思われることでしょう。 しかし、あせって修正しようとしても、経験上なかなかうまくいきません。 不具合を修正したつもりが新たな不具合を引き起こしていたなんてこともありがちですね。
はやる気持ちを抑えて、以下のようにしてみましょう。
不具合を再現できることを確認します。
不具合が発生する最小限のコードを見つけます。例えば、 もしおかしな数値が出力されるのなら、 その数値を計算しているオブジェクトが何なのかを探します。
その不具合のせいで今は失敗する (そして、不具合が修正されたら成功する) テストを書きます。
不具合を修正します。
不具合が再現する最小限のコードを見つける過程で、 不具合の原因がわかるかもしれません。テストを書くことによって、 不具合を真の意味で修正できる可能性が高まるでしょう。なぜなら、 テストを書くことで、将来同じ間違いをする可能性を減らせるからです。 これまでに書いたすべてのテストが、 不注意によって別の問題を発生させる可能性を減らすために役立っているのです。
Unit testing offers many advantages:
Overall, integrated unit testing makes the cost and risk of any individual change smaller. It will allow the project to make [...] major architectural improvements [...] quickly and confidently. 単体テストには、こんなに多くの利点がある。
まとめよう。単体テストをうまく組み込めば、 プログラムを変更する際の手間やリスクをより減らすことになるのだ。 It will allow the project to make [...] major architectural improvements [...] quickly and confidently. | ||
| --Benjamin Smedberg | ||
テストファーストプログラミング、 エクストリームプログラミング、 そして テスト駆動開発 などのソフトウェア開発方法論において、単体テストは非常に重要な位置を占めています。 また、構造上この手法に対応できない言語については 規約による設計 (Design-by-Contract) という手法も認めています。
プログラムを書き終えてから PHPUnit でテストを書くこともできます。 が、テストを書き始めるのが早ければ早いほど、テストの価値が高くなります。 コードが「完成」して何ヶ月もたってからテストを書き始めるのではなく、数日後、 数時間後、いやもうひとがんばりして数分後に書き始めることだってできるでしょう。 さらにもう一歩先へ進んでみませんか? コードを書き始める前にテストを書いたっていいんじゃないですか?
エクストリームプログラミングやテスト駆動開発における 「テストファーストプログラミング」はこの考えに基づいたもので、 さらにそれを究極まで推し進めたものです。現在のコンピュータの能力をもってすれば、 一日に何千ものテストを何千回も繰り返すことだって可能です。 これらのテスト結果を活用することで、 プログラムを少しずつ確実に作成することができるようになります。 テストを自動化すると、新しく追加したテストだけでなく これまでのテストもすべて実行できることが保証されるのです。 テストとはハーケン (登山のときにザイルを通したりする頭部に穴の開いた鋼鉄製の釘) のようなもので、何が起ころうともこの段階までは確実に完成しているということを保証してくれます。
最初にテストを書き始めたときは、おそらくそれを実行できないでしょう。 だって、まだ実装していないオブジェクトやメソッドを使用しているのだから。 最初のうちはこれを気持ち悪く感じるかもしれません。でもそのうちに慣れてきます。 テストファーストプログラミングというのは、オブジェクト指向開発の原則である 「実装をプログラミングするのではなくインターフェイスをプログラミングする」 に従うための実践的な手法であると考えましょう。テストを書いている間、 あなたはきっとテスト対象オブジェクトのインターフェイス (このオブジェクトは、 外部からはどのように見えるのか) について考えていることでしょう。 テストが実際に動作するようになったら、そこで実装のことを考え始めます。 出来上がったテストによって、この段階でインターフェイスは確定しています。
The point of Test-Driven Development is to drive out the functionality the software actually needs, rather than what the programmer thinks it probably ought to have. The way it does this seems at first counterintuitive, if not downright silly, but it not only makes sense, it also quickly becomes a natural and elegant way to develop software. テスト駆動開発 (日本語) のポイントは、プログラマが「こうあるべき」と考える機能ではなく そのソフトウェアが実際に必要としている機能を作り出すことだ。 これは、最初のうちは直感に反するばかばかしいことだと感じるかもしれない。 しかし、これは合理的なものであり、近いうちに 自然でエレガントなソフトウェア開発手法となるだろう。 | ||
| --Dan North | ||
この後に続くテスト駆動開発の例は、やむを得ず簡潔なものになっています。 詳細については Kent Beck の Test-Driven Development [Beck2002] [Beck2002-ja] や Dave Astels の A Practical Guide to Test-Driven Development [Astels2003] などの書籍を参照ください。
この節では、銀行口座を表すクラスを例にして考えます。預金残高の取得や設定、
預け入れや引き落としなどのメソッドだけでなく、BankAccount
クラスは以下のふたつの規約を満たす必要があります。
預金残高の初期値はゼロでなければならない。
預金残高がゼロ未満になってはならない。
まずは BankAccount
クラスのテストを作成し、その後で実際のコードを書いていくようにしましょう。
上の規約をテスト作成の基準とし、それにしたがって
例 13.1
のようにテストメソッドの名前をつけます。
例 13.1: BankAccount クラスのテスト
<?php
require_once 'PHPUnit/Framework.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()
メソッドを
例 13.2
のように実装することです。
例 13.2: テスト testBalanceIsInitiallyZero() をクリアするために必要なコード
<?php
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
}
?>
これで最初のテストはクリアすることになりましたが、 2 番目のテストには失敗します。なぜなら、 テストメソッド内でコールしているメソッドがまだ実装されていないからです。
phpunit BankAccountTest
PHPUnit 3.3.0 by Sebastian Bergmann.
.
Fatal error: Call to undefined method BankAccount::withdrawMoney()
ふたつめの規約のテストをクリアするには、withdrawMoney()、
depositMoney() および setBalance()
の各メソッドを
例 13.3
のように実装しなければなりません。これらのメソッドは、
規約に反するような引数でコールされた場合には BankAccountException
を発生させるように実装しています。
例 13.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.3.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
別の方法としては、PHPUnit_Framework_Assert
クラスが提供する静的なアサーションメソッドを用いて、コード内に
「規約による設計」方式のアサーションを記述するというものもあります。
例 13.4
がその例です。これらのアサーションのいずれかに失敗すると、例外
PHPUnit_Framework_AssertionFailedError が発生します。
この方式を用いると、条件チェックのコードを減らすことができてテストが読みやすくなります。
ただ、プログラムの実行時にも PHPUnit が必要になってしまいます。
例 13.4: 「規約による設計」のアサーションを使用した BankAccount クラス
<?php
require_once 'PHPUnit/Framework.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 は次のように述べています。
エクストリーム・プログラミング (日本語) は本来、壊れる可能性のあるものはすべてテストするという決まりがあった。
今ではしかし、エクストリーム・プログラミングにおけるテスト手法は テスト駆動開発 (日本語) に進化した (第 13 章 を参照ください)。
しかし、各種ツールは未だにテストの語彙で考えることを強要し、 スペックではなくアサーションで考えさせようとする。
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
クラスはストーリーフレームワークを提供します。
これは、振舞駆動開発のための
ドメイン特化言語
(日本語)
の定義を支援するものです。
シナリオ (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 投で倒したピンの数
例 14.1
は、上にまとめたルールを
PHPUnit_Extensions_Story_TestCase
でスペックシナリオとして書き下ろしたものです。
例 14.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 --story BowlingGameSpec
PHPUnit 3.3.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.ユニットテストでコードをテストする方法はわかりました。でも、 テストそのものをテストするにはどうしたらいいのでしょう? テストされていないコードを見つけるには? 言い換えれば、まだテストで カバーされていない部分を見つけるには? 完全にテストができたことをどうやって確認するの? これらのすべての疑問に対する答えとなるのが、コードカバレッジ解析という手法です。 コードカバレッジ解析を行うと、 コードのどの部分がテストされたのかを調べることができるようになります。
PHPUnit のコードカバレッジ解析では、Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。 ステートメントカバレッジというのは、たとえば 100 行のコードで構成されるメソッドがあった場合に、 もしテストで実際に実行されたのがそのうちの 75 行だけだったなら、 そのメソッドのコードカバレッジは 75 パーセントだと考えるということです。
それでは、例 13.3
の BankAccount クラスについての
コードカバレッジレポートを作成してみましょう。
phpunit --coverage-html ./report BankAccountTest
PHPUnit 3.3.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests, 3 assertions)
Generating report, this may take a moment.図 15.1 は、コードカバレッジレポートの一部を抜粋したものです。 テスト時に実行された行は、緑色で強調表示されます。 実行可能なコードであるにもかかわらず実行されなかった行については赤色で強調表示されます。 また、"無意味なコード" についてはオレンジ色で強調表示されます。 行の左にある数字は、その行をカバーするテストの数を表します。
BankAccount のコードカバレッジレポートからわかることは、
setBalance()、depositMoney()
をコールするテストがまだ存在しないということ、
そして withdrawMoney()
に正しい値を指定した場合のテストも存在しないということです。
BankAccountTest クラスに追加するテストを
例 15.1
に示します。これによって、BankAccount
クラスのテストケースを完全に網羅できるようになります。
例 15.1: 完全なコードカバレッジを達成するために欠けているテスト
<?php
require_once 'PHPUnit/Framework.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());
}
}
?>
図 15.2 は、
テストを追加した後の setBalance() のコードカバレッジです。
テストコードで @covers アノテーション
(表 15.1
を参照ください) を使用すると、
そのテストメソッドがどのメソッドをテストしたいのかを指定することができます。
これを指定すると、指定したメソッドのコードカバレッジ情報のみを考慮します。
例 15.2
に例を示します。
例 15.2: どのメソッドを対象とするかを指定したテスト
<?php
require_once 'PHPUnit/Framework.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());
}
}
?>
表15.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 メソッドをカバーすることを表します。 |
どうしてもテストができないコードブロックなどを、
コードカバレッジ解析時に無視させたいこともあるでしょう。
PHPUnit でこれを実現するには、
@codeCoverageIgnoreStart および
@codeCoverageIgnoreEnd アノテーションを
例 15.3
のように使用します。
例 15.3: @codeCoverageIgnoreStart および @codeCoverageIgnoreEnd アノテーションの使用法
<?php
class Sample
{
// ...
public function doSomething()
{
if (0) {
// @codeCoverageIgnoreStart
$this->doSomethingElse();
// @codeCoverageIgnoreEnd
}
}
// ...
}
?>
@codeCoverageIgnoreStart アノテーションから
@codeCoverageIgnoreEnd アノテーションまでの間の行は、
(たとえ実行されなかったとしても) 実行されたものとみなされ、
強調表示されません。
デフォルトでは、1 行でもコードが実行されたソースコードファイルはすべて
(そしてそのようなファイルのみが) レポートに含められます。
レポートにどのようなソースコードファイルを含めるかを設定するには
PHPUnit_Util_Filter API を使用します
(表 15.2
を参照ください)。
表15.2 PHPUnit_Util_Filter の API
| メソッド | 意味 |
|---|---|
void addDirectoryToFilter(string $directory) | あるディレクトリ内でファイル名の最後が .php であるすべてのファイルを (再帰的に) ブラックリストに追加します。 |
void addDirectoryToFilter(string $directory, string $suffix) | あるディレクトリ内でファイル名の最後が $suffix であるすべてのファイルを (再帰的に) ブラックリストに追加します。 |
void addFileToFilter(string $filename) | ファイルをブラックリストに追加します。 |
void removeDirectoryFromFilter(string $directory) | あるディレクトリ内でファイル名の最後が .php であるすべてのファイルを (再帰的に) ブラックリストから削除します。 |
void removeDirectoryFromFilter(string $directory, string $suffix) | あるディレクトリ内でファイル名の最後が $suffix であるすべてのファイルを (再帰的に) ブラックリストから削除します。 |
void removeFileFromFilter(string $filename) | ファイルをブラックリストから削除します。 |
void addDirectoryToWhitelist(string $directory) | あるディレクトリ内でファイル名の最後が .php であるすべてのファイルを (再帰的に) ホワイトリストに追加します。 |
void addDirectoryToWhitelist(string $directory, string $suffix) | あるディレクトリ内でファイル名の最後が $suffix であるすべてのファイルを (再帰的に) ホワイトリストに追加します。 |
void addFileToWhitelist(string $filename) | ファイルをホワイトリストに追加します。 |
void removeDirectoryFromWhitelist(string $directory) | あるディレクトリ内でファイル名の最後が .php であるすべてのファイルを (再帰的に) ホワイトリストから削除します。 |
void removeDirectoryFromWhitelist(string $directory, string $suffix) | あるディレクトリ内でファイル名の最後が $suffix であるすべてのファイルを (再帰的に) ホワイトリストから削除します。 |
void removeFileFromWhitelist(string $filename) | ファイルをホワイトリストから削除します。 |
ブラックリストには、PHPUnit 自身のソースコードファイルやテストファイルがデフォルトで登録されています。 ホワイトリストが空 (デフォルト) の場合はブラックリストを使用し、 ホワイトリストが空でない場合はホワイトリストを使用します。 ホワイトリストを使用する場合は、そのファイルが実行されるかどうかにかかわらず リスト内のすべてのファイルがコードカバレッジレポートに追加されます。
自動テストに慣れてくると、 ほかの目的のためにもテストを使いたくなってくることでしょう。 ここではそんな例を説明します。
一般的に、エクストリームプログラミングのようなアジャイルプロセスを採用しているプロジェクトでは、 ドキュメントの内容が実際の設計やコードに追いついていないことが多いものです。 エクストリームプログラミングでは コードの共同所有 (collective code ownership) を要求しており、 すべての開発者がシステム全体の動作を知っておく必要があります。 作成するテストに対して、そのクラスが何を行うべきなのかを示すような 「わかりやすい」名前をつけられるようにさえしておけば、PHPUnit の TestDox 機能を使用して自動的にドキュメントを生成することができます。 このドキュメントにより、開発者たちはプロジェクト内の各クラスが どのようにふるまうべきなのかを知ることができます。
PHPUnit の TestDox 機能は、テストクラス内のすべてのテストメソッドの名前を抽出し、
それを PHP 風のキャメルケースから通常の文に変換します。つまり
testBalanceIsInitiallyZero() が "Balance is initially zero"
のようになるわけです。最後のほうの数字のみが違うメソッド、例えば
testBalanceCannotBecomeNegative() と
testBalanceCannotBecomeNegative2() のようなものが存在した場合は、
文 "Balance cannot become negative" は一度のみ表示され、
全てのテストが成功したことを表します。
BankAccount クラスのアジャイルな文書
(例 13.1
を参照ください) を見てみましょう。
phpunit --testdox BankAccountTest
PHPUnit 3.3.0 by Sebastian Bergmann.
BankAccount
[x] Balance is initially zero
[x] Balance cannot become negative
また、アジャイルな文書を HTML あるいはプレーンテキスト形式で作成してファイルに書き出すこともできます。
この場合は、引数 --testdox-html
あるいは --testdox-text を使用します。
アジャイルな文書は、プロジェクト内であなたが作成しようとしている外部パッケージについて、 このように動作するであるという期待をまとめた文書にもなります。 外部のパッケージを使用するときには、 そのパッケージが期待通りに動作しなくなるというリスクに常にさらされています。 パッケージのバージョンアップにより知らないうちに挙動が変わってしまい、 あなたのコードが動作しなくなる可能性もあります。そのようなことを避けるため、 「このパッケージはこのように動作するはず」 ということを常にテストケースで記述しておくようにします。テストが成功すれば、 期待通りに動作していることがわかります。もし動作仕様をすべてテストで記述できているのなら、 外部パッケージが将来バージョンアップされたとしても何の心配もいりません。 テストをクリアしたということは、システムは期待通りに動作するということだからです。
あるパッケージについての機能を文書化するためにテストを書いているとき、 そのテストの所有者はあなたです。今あなたがテストを作成しているパッケージの作者は、 そのテストのことについては何も知りません。パッケージの作者とよりつながりを深めるため、 作成したテストを使用してコミュニケートしたり、 そのテストを使用して共同作業をしたりすることができるでしょう。
あなたが作成したテストを使用してパッケージの作者と共同作業をすることになれば、 テストも共同で書くことになります。そうすることで、 より多くのテストケースを挙げられるようになるでしょう。 「暗黙の了解」などに頼っていては、共同作業はできません。 テストと同時に、あなたはそのパッケージに対して期待していることを正確に文書化することになります。 また、すべてのテストにクリアした時点で、 作者はパッケージが完成したことを知ることになります。
スタブ (本書の前のほうで説明した "モックオブジェクト" の章を参照ください) を使用することで、パッケージの作者と別れても作業できるようになります。 パッケージ作者の仕事は、パッケージの実際の実装でテストをクリアするようにすること。 そしてあなたの仕事はあなたが書いたコードでテストをクリアするようにすることです。 この段階になれば、あなたはスタブオブジェクトを使用すればよいのです。 このやり方により、2 つのチームが独立して開発できるようになります。
既存のコードのテストを記述する際は、 以下のようなコードを何度となく繰り返し記述することになるでしょう。
public function testMethod()
{
}PHPUnit は、既存のクラスのコードを解析して テストケースクラスの雛形を作成することができます。
次の例は、Calculator という名前のクラス
(例 17.1 を参照ください)
用のテストクラスの雛形を作成する手順を示すものです。
phpunit --skeleton Calculator
PHPUnit 3.3.0 by Sebastian Bergmann.
Wrote test class skeleton for Calculator to CalculatorTest.php.もとのクラスの各メソッドについて、 出来上がったテストケースクラスのテストケースは不完全な状態 (第 10 章 を参照ください) です。
作成されたテストケースクラスを実行した結果を以下に示します。
phpunit --verbose CalculatorTest
PHPUnit 3.3.0 by Sebastian Bergmann.
CalculatorTest
I
Time: 0 seconds
There was 1 incomplete test:
1) testAdd(CalculatorTest)
This test has not been implemented yet.
/home/sb/CalculatorTest.php:54
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Incomplete: 1.
@assert アノテーションを
メソッドのコメント部で使用すると、
シンプルではあるけれど意味のあるテストを自動的に生成することができます。
これは不完全なテストケースではありません。
例 17.2
に例を示します。
例 17.2: @assert アノテーションをつけた Calculator クラス
<?php
class Calculator
{
/**
* @assert (0, 0) == 0
* @assert (0, 1) == 1
* @assert (1, 0) == 1
* @assert (1, 1) == 2
*/
public function add($a, $b)
{
return $a + $b;
}
}
?>
もとのクラスの各メソッドについて、
@assert アノテーションの内容をチェックします。
これらは、以下のようなテストコードに変換されます。
/**
* Generated from @assert (0, 0) == 0.
*/
public function testAdd() {
$o = new Calculator;
$this->assertEquals(0, $o->add(0, 0));
}
作成されたテストケースクラスを実行した結果を以下に示します。
phpunit CalculatorTest
PHPUnit 3.3.0 by Sebastian Bergmann.
....
Time: 0 seconds
OK (4 tests, 4 assertions)
表 17.1
に、サポートされる @assert
の種類と、それがどのようなテストコードに変換されるかをまとめました。
表17.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 |
テスト駆動開発 (第 13 章 を参照ください) ではまずテストを書いてからそのテストの対象となるコードを書くことになりますが、 PHPUnit ではテストケースクラスをもとにしてクラスの雛形を作成することができます。
規約に従って、Unit クラスのテストは
UnitTest クラスに記述されることになります。
このテストケースクラスのソースを検索し、
Unit クラスのオブジェクトを参照している変数を見つけて
そのオブジェクトがどんなメソッドをコールしているかを調べます。
例として 例 17.4
を見てみましょう。これは、例 17.3
の解析結果をもとにして作成されたものです。
例 17.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());
}
}
?>
例 17.4: 作成された BowlingGame クラスの雛形
<?php
/**
* Generated by PHPUnit on 2008-03-10 at 17:18:33.
*/
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.');
}
}
?>
Selenium RC はテストツールのひとつです。これを使用すると、 ウェブアプリケーションのユーザインターフェイスについてのテストを自動化することができます。 あらゆるプログラミング言語で稼動しているウェブサイトに対応しており、 現在主流のあらゆるブラウザで使用することができます。Selenium RC は Selenium Core を使用しています。これは、ブラウザ上でのタスクを自動的に実行する JavaScript のライブラリです。Selenium でのテストは、 一般のユーザが使用するのと同じようにブラウザ上で直接実行されます。 主な使用例としては、受け入れテスト (各システム単体のテストではなく、結合されたシステム全体に対するテスト) や ブラウザの互換性のテスト (ウェブアプリケーションを、さまざまなオペレーティングシステムやブラウザでテストする) などがあります。
Selenium RC のインストール手順は、次のようになります。
server/selenium-server.jar を /usr/local/bin などにコピーする。java -jar /usr/local/bin/selenium-server.jar などのようにして Selenium RC サーバを起動する。これで、クライアント/サーバ プロトコルを用いて Selenium RC サーバにコマンドを送信できるようになりました。
PHPUnit_Extensions_SeleniumTestCase
は、Selenium RC と通信するための クライアント/サーバ プロトコルを実装したものです。
また、ウェブのテスト用に特化したアサーションメソッドも提供します。
例 18.1 は、
ウェブサイト http://www.example.com/
の <title> 要素の内容をテストする方法を示したものです。
例 18.1: PHPUnit_Extensions_SeleniumTestCase の使用例
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class WebTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser('*firefox');
$this->setBrowserUrl('http://www.example.com/');
}
public function testTitle()
{
$this->open('http://www.example.com/');
$this->assertTitleEquals('Example Web Page');
}
}
?>
PHPUnit_Framework_TestCase クラスとは異なり、
PHPUnit_Extensions_SeleniumTestCase を継承したテストケースクラスは
setUp() メソッドが必須となります。
このメソッド内で、Selenium RC セッションの設定を行います。
ここで使用できるメソッドの一覧は
表 18.1
を参照ください。
表18.1 Selenium RC API: セットアップ
| メソッド | 意味 |
|---|---|
void setBrowser(string $browser) | Selenium RC サーバが使用するブラウザを設定します。 |
void setBrowserUrl(string $browserUrl) | テストするベース URL を設定します。 |
void setHost(string $host) | Selenium RC サーバに接続する際のホスト名を設定します。 |
void setPort(int $port) | Selenium RC サーバに接続する際のポートを設定します。 |
void setTimeout(int $timeout) | Selenium RC サーバに接続する際のタイムアウト値を設定します。 |
void setSleep(int $seconds) | Selenium RC クライアントが、Selenium RC サーバにアクションコマンドを送信してから待機する秒数を設定します。 |
複数のブラウザを使用してテストを行なうこともできます。この場合は、
setBrowser() でブラウザの設定を行うかわりに、
テストケースクラスの中で $browsers という名前の
public static な配列を作成します。
この配列の各項目が個々のブラウザの設定を表します。
これらのブラウザは、それぞれ別の Selenium RC サーバで管理することができます。
例 18.2: 複数のブラウザの設定管理
<?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->assertTitleEquals('Example Web Page');
}
}
?>
PHPUnit_Extensions_SeleniumTestCase を使用すると、
Selenium で実行したテストのカバレッジ情報を収集することができます。
PHPUnit/Extensions/SeleniumTestCase/phpunit_coverage.php をウェブサーバのドキュメントルートディレクトリにコピーします。php.ini ファイルで、PHPUnit/Extensions/SeleniumTestCase/prepend.php と PHPUnit/Extensions/SeleniumTestCase/append.php をそれぞれ auto_prepend_file および auto_append_file に設定します。PHPUnit_Extensions_SeleniumTestCase を継承したテストケースクラスで、protected $coverageScriptUrl = 'http://host/phpunit_coverage.php'; のようにして phpunit_coverage.php スクリプトの URL を指定します。
表 18.2
は、PHPUnit_Extensions_SeleniumTestCase
が提供するさまざまなアサーションメソッドの一覧です。
表18.2 アサーション
| アサーション | 意味 |
|---|---|
void assertAlertPresent() | alert が発生していない場合にエラーを報告します。 |
void assertNoAlertPresent() | alert が発生している場合にエラーを報告します。 |
void assertChecked(string $locator) | $locator で表される要素がチェックされていない場合にエラーを報告します。 |
void assertNotChecked(string $locator) | $locator で表される要素がチェックされている場合にエラーを報告します。 |
void assertConfirmationPresent() | 確認ダイアログが表示されていない場合にエラーを報告します。 |
void assertNoConfirmationPresent() | 確認ダイアログが表示されている場合にエラーを報告します。 |
void assertEditable(string $locator) | $locator で表される要素が編集可能でない場合にエラーを報告します。 |
void assertNotEditable(string $locator) | $locator で表される要素が編集可能な場合にエラーを報告します。 |
void assertElementValueEquals(string $locator, string $text) | $locator で表される要素の値が $text と異なる場合にエラーを報告します。 |
void assertElementValueNotEquals(string $locator, string $text) | $locator で表される要素の値が $text と等しい場合にエラーを報告します。 |
void assertElementContainsText(string $locator, string $text) | $locator で表される要素が $text を含まない場合にエラーを報告します。 |
void assertElementNotContainsText(string $locator, string $text) | $locator で表される要素が $text を含む場合にエラーを報告します。 |
void assertElementPresent(string $locator) | $locator で表される要素が存在しない場合にエラーを報告します。 |
void assertElementNotPresent(string $locator) | $locator で表される要素が存在する場合にエラーを報告します。 |
void assertLocationEquals(string $location) | 現在の位置が $location と異なる場合にエラーを報告します。 |
void assertLocationNotEquals(string $location) | 現在の位置が $location と等しい場合にエラーを報告します。 |
void assertPromptPresent() | プロンプトが表示されていない場合にエラーを報告します。 |
void assertNoPromptPresent() | プロンプトが表示されている場合にエラーを報告します。 |
void 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) | 指定した値が選択されている場合にエラーを報告します。 |
void assertSomethingSelected(string $selectLocator) | $selectLocator で表される項目が選択されていない場合にエラーを報告します。 |
void assertNothingSelected(string $selectLocator) | $selectLocator で表される項目が選択されている場合にエラーを報告します。 |
void assertTextPresent(string $pattern) | 指定したパターン $pattern が存在しない場合にエラーを報告します。 |
void assertTextNotPresent(string $pattern) | 指定したパターン $pattern が存在する場合にエラーを報告します。 |
void assertTitleEquals(string $title) | 現在のタイトルが $title と異なる場合にエラーを報告します。 |
void assertTitleNotEquals(string $title) | 現在のタイトルが $title と等しい場合にエラーを報告します。 |
void assertVisible(string $locator) | $locator で表される要素が不可視な場合にエラーを報告します。 |
void assertNotVisible(string $locator) | $locator で表される要素が可視の場合にエラーを報告します。 |
表 18.3 は、
PHPUnit_Extensions_SeleniumTestCase
のテンプレートメソッドをまとめたものです。
表18.3 テンプレートメソッド
| メソッド | 意味 |
|---|---|
void defaultAssertions() | テストケース内のすべてのテストで共有するアサーションを上書きします。 このメソッドは、Selenium RC サーバにコマンドが送信されるたびに (送信された後に) コールされます。 |
使用できるコマンドのリファレンスや実際の使用法については Selenium Core のドキュメント を参照ください。
runSelenese($filename) メソッドを使用すると、
Selenese/HTML の設定から Selenium のテストを実行することができます。
さらに、静的属性 $seleneseDirectory を使用すると、
Selenese/HTML ファイルを含むディレクトリから自動的にテストオブジェクトを作成することができます。
指定したディレクトリ配下を再帰的に走査し、
.htm ファイルを探します。このファイルには
Selenese/HTML が含まれているものとします。例として
例 18.3
を参照ください。
例 18.3: Selenese/HTML ファイルのディレクトリをテストとして使用する
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class SeleneseTests extends PHPUnit_Extensions_SeleniumTestCase
{
public static $seleneseDirectory = '/path/to/files';
}
?>
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) 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.3.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>PHPUnit は、テストの結果やコードカバレッジのデータを テストデータベース に書き込むことができます。今後、このデータを利用した 新機能 を追加することを検討中です。
テストスイートを 実行 するたびに、run テーブルに行が追加されます。
テストスイート内の テスト が実行されるたびに、test テーブルに行が追加されます。
あるリビジョン内の各 ファイル について、code_file テーブルの中に対応する行が作成されます。
あるリビジョン内のファイルで宣言されている各 クラス について、code_class テーブルの中に対応する行が作成されます。
あるリビジョン内のファイルで宣言されている各 メソッド について、code_method テーブルの中に対応する行が作成されます。
あるリビジョン内のファイルの コードの各行 について、code_line テーブルの中に対応する行が作成されます。
code_coverage テーブルは、テストとそれがカバーするコードの行を関連づけます。
テストの結果やコードカバレッジをデータベースに書き込むには、 用意されているスキーマのいずれかを使用してまず最初にデータベースを作成しなければなりません。
sqlite3 BankAccount.db < PHPUnit/Util/Log/Database/SQLite3.sqlそしてテストスイートを実行し、 テストの結果とコードカバレッジデータをデータベースに書き込みます。
phpunit --test-db-dsn sqlite:///home/sb/BankAccount.db --test-db-log-rev 1 BankAccountTest
PHPUnit 3.3.0 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests)
Storing code coverage data in database, this may take a moment.表 19.1 は、 TextUI テストランナー (第 5 章 を参照ください) でテストデータベース用に指定できる引数をまとめたものです。
表19.1 TextUI のテストデータベース用の引数
| 引数 | 意味 |
|---|---|
--test-db-dsn <dsn> | データベースに接続するための PDO データソース名 (DSN)。 DSN は一般的な形式は、まず PDO ドライバ名、そしてコロン、 その後に各 PDO ドライバ固有の接続書式が続きます。 |
--test-db-log-rev <r> | たとえば Subversion のグローバルリビジョン番号 のような数値で、コードベースの現在のリビジョンを識別するために使用します。 |
--test-db-log-info ... | テスト環境に関する追加情報。 |
この章では、PHPUnit とともに一般的に用いられるビルド自動化ツールについての概要を説明します。
Apache Ant は Java ベースのビルドツールです。理論的には make と同じような種類のツールで、make の古くさい部分を取り除いたものです。Apache Ant のビルドファイルは XML 形式で、さまざまなタスクを定義したターゲットツリーを呼び出します。
例 20.1 は
Apache Ant の build.xml ファイルの例です。これは、組み込みの
<exec> タスクで PHPUnit を実行します。
テストに失敗した場合は、ビルド処理を中断します
(failonerror="true")。
例 20.1: PHPUnit を実行する Apache Ant build.xml ファイル
<project name="Money" default="build">
<target name="clean">
<delete dir="${basedir}/build"/>
</target>
<target name="prepare">
<mkdir dir="${basedir}/build/logs"/>
</target>
<target name="phpunit">
<exec dir="${basedir}" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/phpunit.xml MoneyTest" />
</exec>
</target>
<target name="build" depends="clean,prepare,phpunit"/>
</project>ant
Buildfile: build.xml
clean:
prepare:
[mkdir] Created dir: /home/sb/Money/build/logs
phpunit:
[exec] PHPUnit 3.3.0 by Sebastian Bergmann.
[exec]
[exec] ......................
[exec]
[exec] Time: 0 seconds
[exec]
[exec] OK (22 tests, 34 assertions)
build:
BUILD SUCCESSFUL
Total time: 0 seconds
PHPUnit が作成する XML 形式のテスト結果ログファイル (「テスト結果 (XML)」 を参照ください) は、Apache Ant の <junit>
タスクが使用しているものにもとづいています。
Apache Maven は、ソフトウェアプロジェクトの管理や理解のためのツールです。 プロジェクト指向モデル (POM) にもとづいた Apache Maven は、 プロジェクトのビルドやレポート、ドキュメントの作成を共通の情報から行います。 Maven for PHP は、Maven の威力を用いて PHP プロジェクトのビルドやテスト、ドキュメント作成を行います。
Phing (PHing Is Not GNU make) は、Apache Ant をベースとしたプロジェクトビルドシステムです。 GNU make のような伝統的なビルドシステムができることなら何でもこなせます。 また、シンプルな XML 形式のビルドファイルおよび拡張可能な PHP の "タスク" クラスを採用しており、 使いやすく柔軟なビルドフレームワークとなっています。 機能としては、ファイルの変換 (トークンの置換や XSLT 変換、 Smarty テンプレートによる変換など) やファイルシステム上の操作、 対話的なビルドのサポート、SQL の実行、CVS の操作、 PEAR パッケージの作成用のツールなどがあります。
例 20.2 は
Phing の build.xml ファイルの例です。これは、組み込みの
<phpunit> タスクで PHPUnit を実行します。
テストに失敗した場合は、ビルド処理を中断します
(haltonfailure="true")。
例 20.2: PHPUnit を実行する Phing build.xml ファイル
<project name="Money" default="build">
<target name="clean">
<delete dir="build"/>
</target>
<target name="prepare">
<mkdir dir="build/logs"/>
</target>
<target name="phpunit">
<phpunit printsummary="true" haltonfailure="true">
<formatter todir="build/logs" type="xml"/>
<batchtest>
<fileset dir=".">
<include name="*Test.php"/>
</fileset>
</batchtest>
</phpunit>
</target>
<target name="build" depends="clean,prepare,phpunit"/>
</project>phing
Buildfile: /home/sb/Money/build.xml
Money > clean:
Money > prepare:
[mkdir] Created dir: /home/sb/Money/build/logs
Money > phpunit:
[phpunit] Test: MoneyTest, Run: 22, Failures: 0, Errors: 0,
Incomplete: 0, Skipped: 0, Time elapsed: 0.06887 s
Money > build:
BUILD FINISHED
Total time: 0.2632 secondsこの章では、まず継続的インテグレーションという技法の概要について述べ、 それを PHPUnit にどのように適用するかを説明していきます。
Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily, leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly. 継続的インテグレーション というソフトウェア開発手法は、チームのメンバーどうしが お互いの開発内容を頻繁に結合させるというものだ、通常は、 最低でも一日に一度、あるいは場合によっては一日に何度もこれを行う。 結合の際には自動ビルド (テストを含む) を行い、 エラーはできるだけ早い段階で検出する。 多くのチームが、この手法によって結合時の問題を激減させている。 また、ソフトウェアをより高速に開発できるようにもなっている。 | ||
| --Martin Fowler | ||
継続的インテグレーションでは、テストを含めたビルド手順を 完全に自動化して何度でも実行できるようにしておく必要があります。 これは、一日に何度も実行されます。 各開発者は、この仕組みによって結合を行い、結合時の問題を減らします。 cronjob を使用して自動化を実現することもできます。まずプロジェクトの ソースコードリポジトリ から定期的に最新版をチェックアウトし、 テストを実行し、その結果を利用しやすい形式で出力するというジョブを作成すればよいのです。 ただ、もう少しましなやりかたもありそうです。
Atlassian Bamboo は継続的インテグレーション (Continuous Integration: CI) サーバで、 ソフトウェア開発チームを支援するための機能を提供します。 自動化されたビルド、ソフトウェアのソースコードの状態のテスト、 ビルドが成功したか失敗したかの判定、統計解析データの出力といった機能があります。
次の例では、Bamboo の配布アーカイブが
/usr/local/Bamboo.
に展開されたことを想定しています。
cd /usr/local/Bamboowebapp/WEB-INF/classes/bamboo-init.properties ファイルを編集します。
オプションで bamboo-checkstyle プラグインをインストールします。
./bamboo.sh starthttp://localhost:8085/ をブラウザで開きます。インストールガイドの指示に従います。
Apache Ant を管理パネル内で Builder として設定します。
これで、Bamboo の設定が完了し、プロジェクトのプランを用意できるようになりました。
しかしながら、まずはプロジェクトが必要です。この例では、PHPUnit に同梱されている
Money サンプルのコピーが Subversion リポジトリ
(file:///var/svn/money) にあるものとします。
*.php ファイル群とともに、次のような
Apache Ant ビルドスクリプト (build.xml)
もリポジトリに置きます。
例 21.2: build.xml
<project name="Money" default="build">
<target name="clean">
<delete dir="${basedir}/build"/>
</target>
<target name="prepare">
<mkdir dir="${basedir}/build"/>
<mkdir dir="${basedir}/build/logs"/>
</target>
<target name="phpcs">
<exec dir="${basedir}"
executable="phpcs"
output="${basedir}/build/logs/checkstyle.xml"
failonerror="false">
<arg line="--report=checkstyle ." />
</exec>
</target>
<target name="phpunit">
<exec dir="${basedir}" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/phpunit.xml
--coverage-clover ${basedir}/build/logs/clover.xml
--coverage-html ${basedir}/build/coverage
MoneyTest" />
</exec>
</target>
<target name="build" depends="clean,prepare,phpcs,phpunit"/>
</project>これでプロジェクトができあがりました。Bamboo でプランを作成してみましょう。
http://localhost:8080/ をブラウザで開きます。"Create a Plan" を選び、指示に従います。
"Create a Plan" のステップ 3 で "The build will produce test results" と "Clover output will be produced" をチェックし、PHPUnit が作成した XML ファイルのパスを指定します。
bamboo-checkstyle プラグインをインストールした場合は "CheckStyle output will be produced" もチェックし、PHP_CodeSniffer が作成した XML ファイルのパスを指定します。
"Create a Plan" のステップ 5 で、PHPUnit が作成する HTML ファイルを設定します。
CruiseControl は継続的ビルドプロセス用のフレームワークです。それだけではなく、 メールでの通知や Apache Ant との統合、 またさまざまなバージョン管理ツールとの統合のためのプラグインが用意されています。 またウェブインターフェイスも用意されており、 最新のビルドおよび以前のビルドについての詳細を見ることができます。
次の例では、CruiseControl が
/usr/local/cruisecontrol
にインストールされていることを想定しています。
cd /usr/local/cruisecontrolmkdir -p projects/Money/build/logscd projects/Moneyfile:///var/svn/money sourcebuild.xml ファイルを編集します。例 21.3: projects/Money/build.xml
<project name="Money" default="build" basedir=".">
<target name="checkout">
<exec dir="${basedir}/source/" executable="svn">
<arg line="up"/>
</exec>
</target>
<target name="test">
<exec dir="${basedir}/source" executable="phpunit" failonerror="true">
<arg line="--log-xml ${basedir}/build/logs/phpunit.xml MoneyTest"/>
</exec>
</target>
<target name="build" depends="checkout,test"/>
</project>cd /usr/local/cruisecontrolconfig.xml ファイルを編集します。例 21.4: config.xml
<cruisecontrol>
<project name="Money" buildafterfailed="false">
<plugin
name="svnbootstrapper"
classname="net.sourceforge.cruisecontrol.bootstrappers.SVNBootstrapper"/>
<plugin
name="svn"
classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN"/>
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<bootstrappers>
<svnbootstrapper localWorkingCopy="projects/${project.name}/source/"/>
</bootstrappers>
<modificationset>
<svn localWorkingCopy="projects/${project.name}/source/"/>
</modificationset>
<schedule interval="300">
<ant
anthome="apache-ant-1.7.0"
buildfile="projects/${project.name}/build.xml"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<currentbuildstatuspublisher
file="logs/${project.name}/buildstatus.txt"/>
<email
mailhost="localhost"
buildresultsurl="http://cruise.example.com/buildresults/${project.name}"
skipusers="true"
spamwhilebroken="true"
returnaddress="project@example.com">
<failure address="dev@lists.example.com" reportWhenFixed="true"/>
</email>
</publishers>
</project>
</cruisecontrol>これで CruiseControl サーバを(再)起動できるようになります。
./cruisecontrol.shhttp://localhost:8080/ をブラウザで開きます。
phpUnderControl は
CruiseControl の拡張で、さまざまな PHP 開発ツール、たとえばテスト用の
PHPUnit や
静的コード解析用の
PHP_CodeSniffer、
そして API
ドキュメントの生成用の
PHPDocumentor
を統合します。強力なコマンドラインツールも付属しており、あなたのプロジェクト用に
CruiseControl の XML 設定ファイルを自動生成させることもできます。
次の例では、CruiseControl が
/usr/local/cruisecontrol
にインストールされていることを想定しています。
pear install --alldeps phpunit/phpUnderControlphpuc install /usr/local/cruisecontrolphpuc project --version-control svn
--version-control-url file:///var/svn/money
--test-case MoneyTest
--test-file MoneyTest.php
--test-dir .
--project-name Money
/usr/local/cruisecontrol
上のコマンドは、まずプロジェクトのディレクトリとそのプロジェクト用の設定ファイル
build.xml を作成し、ソースリポジトリから初期チェックアウトを行い、
その新しいプロジェクトをグローバル設定ファイル
config.xml に追加します。
これで CruiseControl サーバを(再)起動できるようになります。
cd /usr/local/cruisecontrol./cruisecontrol.shhttp://localhost:8080/ をブラウザで開きます。
たいていの場合は、PHPUnit の API は単純なものです。単に
PHPUnit_Framework_TestCase を継承したテストケースを作成し、
assertTrue() あるいは assertEquals()
をコールすればよいのです。しかし、PHPUnit をより深く知りたい方のために、
ここではすべてのクラスおよび公開メソッドを説明します。
ほとんどの場合、PHPUnit を使用する際には以下の 5 つのクラスやインターフェイスに出会うことになるでしょう。
PHPUnit_Framework_Assert実際の値が想定した値どおりかどうかを調べるための静的メソッドを集めたもの。
PHPUnit_Framework_Testテストケースとして動作するすべてのオブジェクトのインターフェイス。
PHPUnit_Framework_TestCaseひとつのテスト。
PHPUnit_Framework_TestSuiteテストの集まり。
PHPUnit_Framework_TestResultひとつあるいは複数のテストの実行結果をまとめたもの。
PHPUnit の、5 つの基本クラス/インターフェイスである
PHPUnit_Framework_Assert、
PHPUnit_Framework_Test、
PHPUnit_Framework_TestCase、
PHPUnit_Framework_TestSuite およびand
PHPUnit_Framework_TestResult の関係を
図 22.1
に示します。
PHPUnit 用に書かれたテストケースのほとんどは、間接的に
PHPUnit_Framework_Assert
を継承しています。ここには、
値を自動的にチェックして矛盾を報告するためのメソッドが含まれています。
これらのメソッドは静的に宣言されているので、
あなたが作成したメソッドの中で「規約による設計」方式のアサーションを使用し、
PHPUnit に結果を報告させることができます
(例 22.1 を参照ください)。
例 22.1: 「規約による設計」方式のアサーション
<?php
require_once 'PHPUnit/Framework.php';
class Sample
{
public function aSampleMethod($object)
{
PHPUnit_Framework_Assert::assertNotNull($object);
}
}
$sample = new Sample;
$sample->aSampleMethod(NULL);
?>
Fatal error: Uncaught exception 'PHPUnit_Framework_ExpectationFailedException' with message 'Failed asserting that <null> is not identical to <null>'.
しかし、ほとんどの場合はこれらのアサーションはテストの中で行います。
各アサーションメソッドには 2 種類の方式があります。 エラー時に表示されるメッセージをパラメータとして指定する方法としない方法です。 オプションで指定したメッセージは、通常はテストが失敗したことが報告される場面で 表示されます。これにより、デバッグが楽になります。
例 22.2: メッセージつきのアサーション
<?php
require_once 'PHPUnit/Framework.php';
class MessageTest extends PHPUnit_Framework_TestCase
{
public function testMessage()
{
$this->assertTrue(FALSE, 'これは独自のメッセージです。');
}
}
?>
以下の例は、
例 22.2 のテスト
testMessage()
でメッセージつきのアサーションを使用した場合の出力結果です。
phpunit MessageTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testMessage(MessageTest)
これは独自のメッセージです。
Failed asserting that <boolean:false> is true.
/home/sb/MessageTest.php:8
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.以下に、すべてのアサーションをまとめます。
assertArrayHasKey(mixed $key, array $array[, string $message = ''])
$array にキー $key が存在しない場合にエラー $message を報告します。
assertArrayNotHasKey() はこのアサーションの逆で、同じ引数をとります。
例 22.3: assertArrayHasKey() の使用法
<?php
class ArrayHasKeyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertArrayHasKey('foo', array('bar' => 'baz'));
}
}
?>
phpunit ArrayHasKeyTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testFailure(ArrayHasKeyTest)
Failed asserting that an array has the key <string:foo>.
/home/sb/ArrayHasKeyTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertClassHasAttribute(string $attributeName, string $className[, string $message = ''])
$className::attributeName が存在しない場合にエラー $message を報告します。
assertClassNotHasAttribute() はこのアサーションの逆で、同じ引数をとります。
例 22.4: assertClassHasAttribute() の使用法
<?php
class ClassHasAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertClassHasAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassHasAttributeTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testFailure(ClassHasAttributeTest)
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() はこのアサーションの逆で、同じ引数をとります。
例 22.5: assertClassHasStaticAttribute() の使用法
<?php
class ClassHasStaticAttributeTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertClassHasStaticAttribute('foo', 'stdClass');
}
}
?>
phpunit ClassHasStaticAttributeTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testFailure(ClassHasStaticAttributeTest)
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 として使用することができます。
例 22.6: assertContains() の使用法
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContains(4, array(1, 2, 3));
}
}
?>
phpunit ContainsTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testFailure(ContainsTest)
Failed asserting that an array contains <integer:4>.
/home/sb/ContainsTest.php:6
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.assertContains(string $needle, string $haystack[, string $message = ''])
$needle が $haystack の部分文字列でない場合にエラー $message を報告します。
例 22.7: assertContains() の使用法
<?php
class ContainsTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContains('baz', 'foobar');
}
}
?>
phpunit ContainsTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testFailure(ContainsTest)
Failed asserting that <string: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 属性を実際の値として使用することができます。
例 22.8: assertContainsOnly() の使用法
<?php
class ContainsOnlyTest extends PHPUnit_Framework_TestCase
{
public function testFailure()
{
$this->assertContainsOnly('string', array('1', '2', 3));
}
}
?>
phpunit ContainsOnlyTest
PHPUnit 3.3.0 by Sebastian Bergmann.
F
Time: 0 seconds
There was 1 failure:
1) testFailure(ContainsOnlyTest)
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.assertEqualXMLStructure(DOMNode $expectedNode, DOMNode $actualNode[, boolean $checkAttributes = FALSE, string $message = ''])
XXX