ユニット・テストを使用して、あらゆるレベルで PHP コードをチェックする

モジュール、データベース、UI レベルでの PHP コードのユニット・テスト

テスト主導型の開発とユニット・テストは、変更やリファクタリングを行った後でもコードが期待通りに動作することを確認するための最新の方法です。モジュール、データベース、そしてユーザー・インターフェース(UI) レベルで PHP コードのユニット・テストを行う方法を覚えてください。

Jack Herrington (jherr@pobox.com), Editor-in-Chief, Code Generation Network

Jack D. Herringtonは、20年以上の経験を持つシニア・ソフトウェア・エンジニアです。著者には、「Code Generation in Action」、「Podcasting Hacks」、そして近々刊行予定の「PHP Hacks」の3冊があります。彼は30本以上の技術記事も執筆しています。



2006年 8月 15日

午前 3 時、コードはまだ機能しているでしょうか?

Web アプリケーションは毎日 24 時間無休で実行し続けるものですが、アプリケーションがまだ実行しているかどうかという不安が、かつて私の睡眠を妨げていました。そんな不安を吹き払ってくれたのは、ユニット・テストです。おかげで自分のコードに十分自信が持てるようになり、もちろん夜もぐっすり眠れるようになりました。

ユニット・テストは、コード上にテストを書き込み、これらのテストを自動的に実行するためのフレームワークです。テスト主導型開発はユニット・テストの方法であり、最初にテストを作成し、テストでエラーが検出されることを確認してから、テストに合格するために必要なコードを作成するというものです。すべてのテストに合格すると、開発中の機能が完全であることになります。このようなユニット・テストの価値は、いつでも好きなタイミングでテストを実行できることです。テストを実行するタイミングは、コードをチェックする前でも、大幅なリファクタリングの後でも、あるいは稼働中のシステムにデプロイした後でも構いません。

PHP のユニット・テスト

PHP の場合、ユニット・テストのフレームワークとなるのは PHPUnit2 です。PEARコマンドラインで % pear install PHPUnit2 を実行すると、このシステムが PEAR モジュールとしてインストールされます。

フレームワークのインストールが済んだら、PHPUnit2_Framework_TestCase から派生するテスト・クラスを作成してユニット・テストを書き始めることができます。


モジュールのユニット・テスト

私がユニット・テストの出発点として最適だと判断したのは、アプリケーションのビジネス・ロジック・モジュールです。単純な例として、2つの数値を追加する関数で説明しましょう。テストを開始するため、まず、以下のようなテストを作成します。

リスト 1. TestAdd.php
<?php
require_once 'Add.php';
require_once 'PHPUnit2/Framework/TestCase.php';

class TestAdd extends PHPUnit2_Framework_TestCase
{
function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); }
function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }
}
?>

TestAdd クラスには 2 つのメソッドが含まれ、どちらも test という接頭辞が付いています。それぞれのメソッドがテストを定義し、そのテストはリスト1 のようにシンプルにすることも、開発中の機能を特定の観点から完全にテストするのに必要なだけ複雑にすることもできます。この例では単純に、最初のテストで1 + 2 = 3 になること、2 番目のテストで 1 + 1 = 2 になることを表明しています。

PHPUnit2 システムが定義している assertTrue() メソッドを使用して、引数に含まれる条件が true となることをテストします。次に、テストで最初に不合格となるコードを十分に実装したAdd.php モジュールを作成します。

リスト 2. Add.php
<?php
function add( $a, $b ) { return 0; }
?>

ここでユニット・テストを実行すると、両方のテストに失敗します。

リスト 3. テストの失敗
% phpunit TestAdd.php
PHPUnit 2.2.1 by Sebastian Bergmann.

FF

Time: 0.0031270980834961
There were 2 failures:
1) test1(TestAdd)

2) test2(TestAdd)


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

これで、両方のテストがその役目を果たすことが分かりました。そこで、add() 関数が実際に正しく働くように変更します。

<?php
function add( $a, $b ) { return $a+$b; }
?>

今度は両方のテストに合格します。

リスト 4. テストの合格
% phpunit TestAdd.php
PHPUnit 2.2.1 by Sebastian Bergmann.

..

Time: 0.0023679733276367

OK (2 tests)
%

このテスト主導型開発の例は非常に単純なものですが、要点を示しています。まず、テストとテストを実行するのに十分なコード(テストの結果は失敗ですが) を作成します。次に、テストに失敗することを確認し、今度はテストに合格するコードを実装します。

コードを実装するたびに、さらにテストを追加することになります。これは、テストのセットがコード・パスに含まれるすべてのバリアントをチェックする完全なものになるまで続きます。作成するテストについての提案、そしてその作成方法については、この記事の終わりで説明します。


データベースのテスト

モジュールのテストが終わったら、データベース・アクセスをテストします。データベース・アクセスのテストには、いくつかの問題があります。まず、各テストの前に、データベースを既知の時点にリセットしなければなりません。次に、このリセットによって実際のデータベースが損傷する恐れがあるため、実動データベースとは異なるデータベースでテストするか、または既存のデータベース・コンテンツに影響しないようなテストを作成する必要があります。

データベースのユニット・テストは、データベースから始めます。これを説明するため、以下のような単純なスキーマを使用します。

リスト 5. Schema.sql
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name TEXT NOT NULL,
PRIMARY KEY ( id )
);

リスト 5 は、作成者とそれぞれに関連付けられた ID のテーブルです。

次に、テストを作成します。

リスト 6. TestAuthors.php
<?php
require_once 'dblib.php';
require_once 'PHPUnit2/Framework/TestCase.php';

class TestAuthors extends PHPUnit2_Framework_TestCase
{
function test_delete_all() {
$this->assertTrue( Authors::delete_all() );
}
function test_insert() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
}
function test_insert_and_get() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
$this->assertTrue( Authors::insert( 'Joe' ) );
$found = Authors::get_all();
$this->assertTrue( $found != null );
$this->assertTrue( count( $found ) == 2 );
}
}
?>

このテスト・セットは、テーブルから作成者を削除し、テーブルに作成者を追加し、そして作成者がテーブルに含まれているかどうかを検証した上で作成者を挿入するという内容を網羅しています。これは、エラーを検出するのに便利な追加式階層のテストです。どのテストが上手くいき、どのテストが上手くいかなかったかを確認し、その違いを理解することによって、何が誤っているのかがすぐに分かります。

以下に、dblib.php PHP データベース・アクセス・コードの最初の失敗バージョンを示します。

リスト 7. Dblib.php
<?php
require_once('DB.php');

class Authors
{
public static function get_db()
{
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete_all()
{
return false;
}
public static function insert( $name )
{
return false;
}
public static function get_all()
{
return null;
}
}
?>

リスト 8 のコードでユニット・テストを実行すると、3 つすべてのテストに失敗します。

リスト 8. Dblib.php
% phpunit TestAuthors.php
PHPUnit 2.2.1 by Sebastian Bergmann.

FFF

Time: 0.007500171661377
There were 3 failures:
1) test_delete_all(TestAuthors)

2) test_insert(TestAuthors)

3) test_insert_and_get(TestAuthors)


FAILURES!!!
Tests run: 3, Failures: 3, Errors: 0, Incomplete Tests: 0.
%

これで、コードを追加できます。3 つすべてのテストに合格するまで、データベースに正しくアクセスするコードをメソッドごとに追加していきます。dblib.phpコードの最終バージョンは、以下のようになります。

リスト 9. 完成した dblib.php
<?php
require_once('DB.php');

class Authors
{
public static function get_db()
{
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete_all()
{
$db = Authors::get_db();
$sth = $db->prepare( 'DELETE FROM authors' );
$db->execute( $sth );
return true;
}
public static function insert( $name )
{
$db = Authors::get_db();
$sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' );
$db->execute( $sth, array( $name ) );
return true;
}
public static function get_all()
{
$db = Authors::get_db();
$res = $db->query( "SELECT * FROM authors" );
$rows = array();
while( $res->fetchInto( $row ) ) { $rows []= $row; }
return $rows;
}
}
?>

このコードでテストを実行すると、すべてのテストがエラーなしで実行され、コードが正しく機能していることが分かります。


HTML のテスト

PHP アプリケーション全体をテストする上で必要な次のステップは、フロントエンドのHTML (Hypertext Markup Language) インターフェースをテストすることです。この目的のため、以下のようなWeb ページを使用します。

図 1. テスト Web ページ
図 1. テスト Web ページ

このページは、2 つの数値を加算します。ページをテストするには、ユニット・テスト・コードから始めます。

リスト 10. TestPage.php
<?php
require_once 'HTTP/Client.php';
require_once 'PHPUnit2/Framework/TestCase.php';

class TestPage extends PHPUnit2_Framework_TestCase
{
function get_page( $url )
{
$client = new HTTP_Client();
$client->get( $url );
$resp = $client->currentResponse();
return $resp['body'];
}
function test_get()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '/<html>/', $page ) == 1 );
}
function test_add()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '/<html>/', $page ) == 1 );
preg_match( '/<span id="result">(.*?)<\/span>/', $page, $out );
$this->assertTrue( $out[1]=='30' );
}
}
?>

このテストには、PEAR の HTTP Client モジュールを使用します。組み込み CURL(PHP Client URL Library) を使うこともできますが、このモジュールの方が多少簡単だと思います。

最初のテストではページの戻りについてチェックし、ページに HTML が含まれるかどうかを判別します。2番目のテストでは、要求に含まれる URL に 10 と 20 の値を書き込んで合計を要求し、ページ内にエンコードされた結果スパンをチェックします。

このページのコードは、以下のようになります。

リスト 11. TestPage.php
<html><body><form>
<input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> +
<input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =
<span id="result"><?php echo($_REQUEST['a']+$_REQUEST['b']); ?></span>
<br/>
<input type="submit" value="Add" />
</form></body></html>

このページは至ってシンプルです。2 つの入力フィールドには、要求に含まれる現行値が示されます。結果スパンには、これらの2 つの数値の合計が示されます。ここで大きな特徴となっているのは、<span> タグです。このタグはユーザーには見えませんが、ユニット・テストにとっては違います。つまり、ユニット・テストは値を検出するための複雑なツリー・ロジックを使用する代わりに、特定の<span> タグの値を取得します。このような方法では、インターフェースが変更されてもスパンがある限りテストに合格します。

ここでも同じく、最初にテストをコード化し、次にページの失敗バージョンを作成します。失敗バージョンをテストしてから、ページが機能するように変更します。結果は以下のようになります。

リスト 12. 失敗バージョンをテストした後に変更されたページ
% phpunit TestPage.php
PHPUnit 2.2.1 by Sebastian Bergmann.

..

Time: 0.25711488723755

OK (2 tests)
%

両方のテストに合格します。つまり、コードは正しく機能しています。

ただし、HTML フロントエンドのテストには罠があります。それは、JavaScriptです。HTTP (Hypertext Transport Protocol) クライアント・コードはページを取得しますが、JavaScriptを実行することはありません。そのため、JavaScript ファイルに多くのコードがある場合は、ユーザー・エージェント・レベルのユニット・テストを作成しなければなりません。私の結論では、その最良の方法となるのはMicrosoft® Internet Explorer® に組み込まれた自動レイヤーを使用することです。PHPで作成された Microsoft Windows® スクリプトは、COM (Component ObjectModel) インターフェースを使用して Internet Explorer を制御できます。これによって、InternetExplorer をページにナビゲートさせ、次に DOM (Document Object Model) メソッドを使用して、特定のユーザー操作によってページの要素がどのようになるかを調べます。

フロントエンドで JavaScript コードのユニット・テストを実行する方法は、これ以外にまだ見つけていません。これは作成するのも管理するのも容易ではなく、また、このようなタイプのテストはページにマイナーな変更を加えると壊れやすいことも確かです。


作成するテストと、その作成方法

私はテストを作成するときに、以下の条件を守るようにしています。

すべての肯定的テスト
このテスト・セットは、すべて正常に機能することを確実にします。
すべての失敗テスト
これらのテストを 1 つずつ使用して、失敗または例外のすべてのケースが上手くいくことを確認します。
肯定的シーケンス・テスト
このテスト・セットは、正しい順序の呼び出しが正常に機能することを確実にします。
否定的シーケンス・テスト
このテスト・セットは、呼び出しの順序が狂った場合に呼び出しが失敗することを確実にします。
負荷テスト
必要に応じて小さなテスト・セットを実行して、これらのテストのパフォーマンスが正常な範囲内であることを判断できます。例えば、2,000の呼び出しは 2 秒以内に処理されなければなりません。
リソース・テスト
これらのテストは、アプリケーション・プログラム・インターフェース (API)が適切にリソースの割り当てと解放を行うことを確実にします。例えば、ある行でファイル・ベースのAPI のオープン、書き込み、クローズを何回か続けて行って、開いたままのファイルがないことを確認します。
コールバック・テスト
コールバック・メソッドを持つ API の場合、これらのテストによって、コールバックが定義されていない場合にコードが正しく実行されることを確認できます。さらに、コールバックが定義されていても、不適切に動作したり例外を生成した場合に、コードが正しく実行されることも確認できます。

上記は、ユニット・テストについてのいくつかのアイデアです。ユニット・テストの作成方法についても、提案があります。

ランダム・データを使用しない
インターフェースでランダム・データをスローするのはいい考えのように思えますが、データのデバッグが困難なため、これは避けるようにしてください。呼び出しの度にデータがランダムに生成されると、ある受け渡しではエラーが発生し、他の受け渡しではエラーが発生しないという可能性があります。テストにランダム・データが必要な場合は、ファイル内にデータを生成し、そのファイルをすべての実行で使用してください。このようにすると、「うるさい」データになるかもしれませんが、エラーをデバッグすることは可能です。
テストをグループ分けする
夢中になって、すべてを実行するのに何時間もかかる何千ものテストを作成するのは簡単です。それはそれで結構ですが、テストをグループ分けして、基礎をチェックするための短時間のテスト・セットを実行してから、セット全部を夜通し実行するようにしてください。
強力な API と強力なテストを作成する
重要なのは、新しい機能を追加したり、既存の機能を変更したときに簡単に壊れないようなAPI とテストを作成することです。問題を解決するための一般的で確実な方法はありませんが、あえて言うなら、不安定なテスト(合格と失敗を繰り返すようなテスト) はすぐに失格になります。

まとめ

ユニット・テストはエンジニアにとって貴重なもので、効率的な開発プロセス(コードを重要視するプロセス) の要の 1 つとなります。なぜなら、文書では、コードが仕様に従って機能するという証拠が必要とされるためです。ユニット・テストはその証拠を提供します。開発プロセスは、コードの実行内容(ただし、今のところ実行していない内容) を定義するユニット・テストから始まります。そのため、すべてのテストは失敗結果を出すところから始まります。コードが完成に近づくにつれ、テストに合格し始めます。すべてのテストに合格した時点で、コードは完成を迎えます。

私はユニット・テストを使わずに、大掛かりなコードを作成したり、大規模なコード・ブロックや複雑なコード・ブロックを作成したことはありません。コードを変更する前に、既存のコードに対するユニット・テストを作成して、変更を加えると何が壊れるか(あるいは壊れないか) を確認することがよくあります。これを確認することが、カスタマーに発送するコードが(午前 3 時であろうと) 正しく実行されているという自信につながります。

参考文献

学ぶために

製品や技術を入手するために

  • IBM ソフトウェアの試用版を使用して、次のオープン・ソース開発プロジェクトを改革してください。ダウンロード、あるいはDVD で入手できます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=237333
ArticleTitle=ユニット・テストを使用して、あらゆるレベルで PHP コードをチェックする
publish-date=08152006