午前 3 時、コードはまだ機能しているでしょうか?
Web アプリケーションは毎日 24 時間無休で実行し続けるものですが、アプリケーションがまだ実行しているかどうかという不安が、かつて私の睡眠を妨げていました。そんな不安を吹き払ってくれたのは、ユニット・テストです。おかげで自分のコードに十分自信が持てるようになり、もちろん夜もぐっすり眠れるようになりました。
ユニット・テストは、コード上にテストを書き込み、これらのテストを自動的に実行するためのフレームワークです。テスト主導型開発はユニット・テストの方法であり、最初にテストを作成し、テストでエラーが検出されることを確認してから、テストに合格するために必要なコードを作成するというものです。すべてのテストに合格すると、開発中の機能が完全であることになります。このようなユニット・テストの価値は、いつでも好きなタイミングでテストを実行できることです。テストを実行するタイミングは、コードをチェックする前でも、大幅なリファクタリングの後でも、あるいは稼働中のシステムにデプロイした後でも構いません。
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;
}
}
?>
|
このコードでテストを実行すると、すべてのテストがエラーなしで実行され、コードが正しく機能していることが分かります。
PHP アプリケーション全体をテストする上で必要な次のステップは、フロントエンドのHTML (Hypertext Markup Language) インターフェースをテストすることです。この目的のため、以下のような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 時であろうと) 正しく実行されているという自信につながります。
学ぶために
-
PHP.net は、PHP コードに関する優れた資料です。
- Kent Back の著書「
Test-Driven Development」などの本は、テストに基づいたこの新しいプログラミング方法を理解する上での手掛かりとなります。
-
C2 の wiki には、ユニット・テストについての豊富な情報が用意されています。
- Sebastian Bergmann の著書「PHPUnit Pocket Guide」では、PHPUnit フレームワークについて手軽に学べます。
-
「PHP Extension and Application Repository」は、PHP 開発者のための優れた資料です。
- IBM developerWorks の PHP プロジェクト・リソースにアクセスして、PHPの詳細を学んでください。
- developerWorks ですべての PHP 関連記事を調べてください。
-
developerWorks technical events and webcastsを利用して、最新技術を学んでください。
- 世界中で近日中に予定されている IBM オープン・ソース開発者を対象とした会議、見本市、ウェブ放送やその他のイベントをチェックしてください。
- オープン・ソース技術を使用して開発し、IBM の製品と併用するときに役立つ広範囲のハウツー情報、ツール、およびプロジェクト・アップデートについては、developerWorks Open source ゾーンを参照してください。
- ソフトウェア開発者を対象とした興味深いインタービューや討論については、developerWorks ポッドキャストをチェックしてください。
製品や技術を入手するために
-
IBM ソフトウェアの試用版を使用して、次のオープン・ソース開発プロジェクトを改革してください。ダウンロード、あるいはDVD で入手できます。
議論するために
-
developerWorks blogsからdeveloperWorksのコミュニティーに加わってください。