テスト駆動開発と単体テストは、変更や大幅な調整にもかかわらず、コードが期待どおりに動作し続けることを確認するための最新の方法です。この記事では、モジュール、データベース、およびユーザー インターフェイス (UI) レイヤーで PHP コードを単体テストする方法を学習します。
午前3時です。コードがまだ動作していることはどうやって確認できるのでしょうか?
Web アプリケーションは 24 時間年中無休で実行されるため、プログラムがまだ動作しているかどうかという疑問が夜になると気になります。単体テストのおかげで、自分のコードに十分な自信が持てるようになり、よく眠れるようになりました。
単体テストは、コードのテスト ケースを作成し、これらのテストを自動的に実行するためのフレームワークです。テスト駆動開発は、まずテストを作成し、そのテストでエラーが検出できることを確認し、その後で初めて、これらのテストに合格する必要があるコードの作成を開始するという考えに基づいた単体テストのアプローチです。すべてのテストに合格すると、開発した機能が完成します。これらの単体テストの価値は、コードをチェックインする前、大きな変更後、または実行中のシステムへの展開後など、いつでも実行できることです。
PHP 単体テスト
PHP の場合、単体テスト フレームワークは PHPUnit2 です。このシステムは、PEAR コマンド ライン % pear install PHPUnit2 を使用して、PEAR モジュールとしてインストールできます。
フレームワークをインストールした後、PHPUnit2_Framework_TestCase から派生したテスト クラスを作成することで単体テストを作成できます。
モジュール単体テスト
単体テストを始めるのに最適な場所は、アプリケーションのビジネス ロジック モジュールであることがわかりました。簡単な例を使用します。これは 2 つの数値を合計する関数です。テストを開始するには、まず以下に示すようにテスト ケースを作成します。
リスト 1. TestAdd.php
<?phprequire_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 つのメソッドがあり、どちらもテスト プレフィックスを使用します。各メソッドはテストを定義します。テストはリスト 1 のように単純なものもあれば、非常に複雑なものもあります。この場合、最初のテストでは 1 プラス 2 は 3 に等しく、2 番目のテストでは 1 プラス 1 は 2 に等しいと単純に主張します。
PHPUnit2 システムは、パラメータに含まれる条件値が true かどうかをテストするために使用されるassertTrue() メソッドを定義します。次に、Add.php モジュールを作成しましたが、最初は誤った結果が生成されました。
リスト 2. Add.php
<?phpfunction add( $a, $b ) { return 0; }?>
単体テストを実行すると、両方のテストが失敗します。
リスト 3. テストの失敗
% phpunit TestAdd.phpPHPUnit 2.2.1 by Sebastian Bergmann.FFTime: 0.0031270980834961失敗が 2 つありました:1) test1(TestAdd)2) test2(TestAdd)FAILURES!!!テストの実行: 2、失敗: 2、エラー: 0、不完全なテスト: 0。
両方のテストが正常に機能することがわかりました。したがって、add() 関数を変更して実際の処理を行うことができます。
両方のテストに合格しました。
<?phpfunction add( $a, $b ) { return $a+$b; }?>
リスト 4. テストは
% phpunit TestAdd.phpPHPUnit 2.2.1 に合格しました (Sebastian Bergmann 作成)...時間: 0.0023679733276367OK (2 テスト)
%このテスト駆動開発の例は非常に単純ですが、その考え方は理解できます。最初にテスト ケースを作成し、テストを実行するのに十分なコードがありましたが、結果は間違っていました。次に、テストが実際に失敗することを確認し、実際のコードを実装してテストを成功させます。
コードを実装するとき、すべてのコード パスをカバーする完全なテストが完了するまで、コードを追加し続けることがわかります。この記事の最後には、作成するテストとその作成方法に関するアドバイスが記載されています。
データベースのテスト
モジュールのテスト後、データベース アクセスのテストを実行できます。データベース アクセスのテストでは、2 つの興味深い問題が生じます。まず、各テストの前にデータベースを既知の時点に復元する必要があります。次に、このリカバリによって既存のデータベースに損傷が生じる可能性があることに注意してください。そのため、テスト ケースを作成する際には、非運用データベースでテストするか、既存のデータベースの内容に影響を与えないように注意する必要があります。
データベース単体テストはデータベースから始まります。この問題を説明するには、次の単純なパターンを使用する必要があります。
リスト 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 は authors テーブルであり、各レコードには関連付けられた ID があります。
次に、テスト ケースを作成できます。
リスト 6. TestAuthors.php
<?phprequire_once 'dblib.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestAuthors extends PHPUnit2_Framework_TestCase{ function test_delete_all() { $this->assertTrue( Authors::delete_all() ); } 関数 test_insert() { $this->assertTrue( 著者::delete_all() ); $this->assertTrue( 著者::insert( 'Jack' ) ); } 関数 test_insert_and_get() { $this->assertTrue( 著者::delete_all() ); $this->assertTrue( 著者::insert( 'Jack' ) ); $this->assertTrue( 著者::insert( 'Joe' ) ); ; $this->assertTrue( $found != null ); $this->assertTrue( count( $found ) == 2 );
この一連のテストは、テーブルからの著者の削除とテーブルへの著者の挿入を対象としています
。著者の存在を確認しながら著者を挿入するなどの機能も。これは累積的なテストであり、バグを見つけるのに非常に役立ちます。どのテストが機能し、どのテストが機能しないかを観察することで、何が問題になっているのかをすぐに特定し、その違いをさらに理解することができます。
最初に障害を引き起こした dblib.php PHP データベース アクセス コードのバージョンを以下に示します。
リスト 7. dblib.php
<?phprequire_once('DB.php'); class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; :Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()) } return $db; } public static function insert( $name ) { return false; } public static function get_all() { return null; }}?>
リスト 8 のコードに対して単体テストを実行すると、3 つのテストがすべて失敗することがわかります
。 php
% phpunit TestAuthors.phpPHPUnit 2.2.1 by Sebastian Bergmann.FFFTime: 0.007500171661377失敗が 3 つありました:1) test_delete_all(TestAuthors)2) test_insert(TestAuthors)3) test_insert_and_get(TestAuthors)FAILURES!!!テストの実行: 3、失敗: 3、エラー: 0、不完全なテスト: 0.%
これで、3 つのテストがすべて合格するまで、メソッドごとにデータベースに正しくアクセスするためのコードの追加を開始できます。 dblib.php コードの最終バージョンを以下に示します。
リスト 9. 完全な dblib.php
<?phprequire_once('DB.php');class Authors{ public static function get_db() { $dsn = 'mysql://root:password@localhost/unitdb'; ::Connect( $dsn, array() ); if (PEAR::isError($db)) { die($db->getMessage()) } return $db; = 著者::get_db(); $sth = $db->prepare( '著者から削除' ); $db->execute( $sth ); } public static function insert( $name );著者::get_db(); $sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' ); $db->execute( $sth, array( $name ) );静的関数 get_all() { $db = 著者::get_db(); $db->query( "SELECT * FROM authors" ); $res->fetchInto( $ row) ) ) { $rows []= $row; } return $rows; }}?>
HTML テスト
PHP アプリケーション全体をテストする次のステップは、フロントエンドのハイパーテキスト マークアップ言語 (HTML) インターフェイスをテストすることです。このテストを実行するには、以下のような Web ページが必要です。
リスト 10. TestPage.php
<?phprequire_once 'HTTP/Client.php';require_once 'PHPUnit2/Framework/TestCase.php';class TestPage extends PHPUnit2_Framework_TestCase{ function get_page( $url ) { $client = new HTTP_Client(); ->get( $url ); $resp = $client->currentResponse(); return $resp['body'] } 関数 test_get() { $page = TestPage::get_page( 'http://localhost/unit /add.php' ); $this->assertTrue( strlen( $page ) > 0 ); $this->assertTrue( preg_match( '/<html>/', $page ) == 1 ); ) { $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' ); $this->assertTrue( strlen( $page ) > 0 ); assertTrue( preg_match( '/<html>/', $page ) == 1 ); preg_match( '/<span id="result">(.*?)</span>/', $page, $out ); $this->assertTrue( $out[1]=='30' ); }}?>
このテストは、PEAR が提供する HTTP クライアント モジュールを使用します。組み込みの PHP クライアント URL ライブラリ (CURL) よりも少し単純だと思いますが、後者も使用できます。
返されたページをチェックして、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 つの値の合計を示します。 マークアップはすべての違いをマークします。ユーザーには表示されませんが、単体テストには表示されます。したがって、単体テストでは、この値を見つけるために複雑なロジックは必要ありません。代わりに、特定のタグの値を取得します。このようにして、インターフェイスが変更されても、スパンが存在する限り、テストは合格します。
前と同様に、最初にテスト ケースを作成してから、ページの失敗したバージョンを作成します。失敗するかどうかをテストし、ページのコンテンツを変更して機能するようにします。結果は次のとおりです。
リスト 12. テストが失敗し、ページを変更する
% phpunit TestPage.phpPHPUnit 2.2.1 by Sebastian Bergmann...Time: 0.25711488723755OK (2 テスト)%
どちらのテストも合格できます。これは、テストが成功したことを意味します。コード 正常に動作します。
このコードでテストを実行すると、すべてのテストが問題なく実行されるため、コードが正しく動作することがわかります。
しかし、HTML フロントエンドのテストには JavaScript という欠陥があります。ハイパーテキスト転送プロトコル (HTTP) クライアント コードはページを取得しますが、JavaScript は実行しません。したがって、JavaScript のコードが大量にある場合は、ユーザー エージェント レベルの単体テストを作成する必要があります。この機能を実現するために私が見つけた最良の方法は、Microsoft® Internet Explorer® に組み込まれているオートメーション レイヤー機能を使用することです。 PHP で記述された Microsoft Windows® スクリプトを使用すると、コンポーネント オブジェクト モデル (COM) インターフェイスを使用して Internet Explorer を制御してページ間を移動し、特定のユーザー アクションを実行した後にドキュメント オブジェクト モデル (DOM) メソッドを使用してページを検索できます。 。
これは、私が知っているフロントエンド JavaScript コードの単体テストの唯一の方法です。書いて維持するのが簡単ではないことは認めますが、ページにわずかな変更が加えられると、これらのテストは簡単に壊れてしまいます。
どのテストを作成するか、どのように作成するか
テストを作成するときは、次のシナリオを取り上げたいと思います。
すべての肯定的なテスト
この一連のテストは、すべてが期待どおりに機能していることを確認します。
すべてのネガティブ テストでは、
これらのテストを 1 つずつ使用して、すべての障害または異常が確実にテストされるようにします。
ポジティブ シーケンス テスト
この一連のテストは、正しいシーケンスでの呼び出しが期待どおりに動作することを確認します。
ネガティブ シーケンス テスト
この一連のテストは、呼び出しが正しい順序で行われない場合に呼び出しが失敗することを確認します。
負荷テスト
必要に応じて、少数のテストを実行して、これらのテストのパフォーマンスが想定内であるかどうかを判断できます。たとえば、2,000 件の呼び出しは 2 秒以内に完了する必要があります。
リソース テスト
これらのテストでは、アプリケーション プログラミング インターフェイス (API) がリソースを正しく割り当て、解放していることを確認します。たとえば、ファイルベースの API を連続して数回呼び出して、開いているファイルがないことを確認します。
コールバック テスト
コールバック メソッドを持つ API の場合、これらのテストは、コールバック関数が定義されていない場合にコードが正常に実行されることを確認します。さらに、これらのテストでは、コールバック関数が定義されていても、そのコールバック関数が正しく動作しない場合や例外が生成された場合でも、コードが正常に実行できることを確認することもできます。
ここでは単体テストについていくつか考えてみます。単体テストの作成方法についていくつかの提案があります:
ランダム データを使用しないでください
。インターフェイスでランダム データを生成するのは良いアイデアのように思えるかもしれませんが、このデータはデバッグが非常に困難になる可能性があるため、それは避けたいと考えています。データが呼び出しごとにランダムに生成される場合、あるテストではエラーが発生しても、別のテストではエラーが発生しない可能性があります。テストにランダム データが必要な場合は、ランダム データをファイルに生成し、実行するたびにそのファイルを使用できます。このアプローチでは、「ノイズの多い」データが得られますが、それでもエラーをデバッグできます。
グループ テスト
実行に数時間かかるテストを数千件も簡単に蓄積できます。それ自体には何も問題はありませんが、これらのテストをグループ化すると、一連のテストをすばやく実行して重大な懸念事項を確認し、その後夜間にテスト全体を実行することができます。
堅牢な API と堅牢なテストを作成する
新しい機能が追加されたり、既存の機能が変更されたりしたときに、簡単に壊れないように API とテストを作成することが重要です。万能の特効薬はありませんが、経験則として、「変動する」(時々失敗し、時々成功し、何度も繰り返す)テストはすぐに破棄する必要があります。
結論
単体テストはエンジニアにとって非常に重要です。これらはアジャイル開発プロセスの基盤です (ドキュメント化にはコードが仕様に従って動作しているという証拠が必要であるため、コーディングに重点が置かれます)。単体テストはこの証拠を提供します。プロセスは単体テストから始まります。単体テストでは、コードが実装する必要があるが現在は実装されていない機能を定義します。したがって、最初はすべてのテストが失敗します。コードがほぼ完成すると、テストに合格します。すべてのテストに合格すると、コードは非常に完成したものになります。
私は、単体テストを使用せずに大規模なコードを書いたり、大規模または複雑なコード ブロックを変更したりしたことはありません。通常、既存のコードを変更する前に単体テストを作成します。これは、コードを変更するときに何が壊れているのか (または壊れていないのか) を確認するためです。これにより、たとえ午前 3 時であっても、クライアントに提供したコードが正しく実行されているという大きな自信が得られます。