基本的なテストメソッドの作成が一段落したところで、テスト対象となるEmployeeクラスを実装していきましょう。
ここで、PHPUnitの便利な機能を1つ紹介しましょう。PHPUnitでは作成済みのテストケースから、対象となるクラスを自動生成する機能が備わっています。今回の例ではEmployeeクラスを自動生成できます。
テストケース内ではすでにEmployeeクラスが存在する前提で、$this->employeeオブジェクトに対してさまざまなメソッドを呼び出しています。PHPUnitではこれを自動検知し、クラスと必要なメソッドを定義したファイルを自動生成します。
方法は簡単で、テストケースファイルと同一ディレクトリに移動し、phpunitコマンドを使い、--skeleton-classオプションに続いてテストケースのクラス名を入力するだけです。
% phpunit --skeleton-class EmployeeTest [Enter]
PHPUnit 3.4.1 by Sebastian Bergmann. Wrote skeleton for "Employee" to "./Employee.php".
もちろん中のロジックは実装されていませんが、次のようにテストケースで必要なメソッドを定義したクラスが一瞬で作成されるのです。
<?php /** * Generated by PHPUnit on 2009-10-22 at 13:07:33. */ class Employee { protected $age, $name; /** * @todo Implement getAge(). */ public function getAge() { return $this->age; // Remove the following line when you implement this method. throw new RuntimeException('Not yet implemented.'); } /** * @todo Implement getName(). */ public function getName() { return $this->name; // Remove the following line when you implement this method. throw new RuntimeException('Not yet implemented.'); } /** * @todo Implement getPrimaryKey(). */ public function getPrimaryKey() { return 1; // Remove the following line when you implement this method. throw new RuntimeException('Not yet implemented.'); } /** * @todo Implement save(). */ public function save() { return true; // Remove the following line when you implement this method. throw new RuntimeException('Not yet implemented.'); } /** * @todo Implement setAge(). */ public function setAge($age) { $this->age = $age; // Remove the following line when you implement this method. //throw new RuntimeException('Not yet implemented.'); } /** * @todo Implement setName(). */ public function setName($name) { $this->name = $name; // Remove the following line when you implement this method. //throw new RuntimeException('Not yet implemented.'); } }?>
Employeeクラスが出来上がればテストの実行が可能になります。テストの実行はphpunitコマンドにテストケースファイルパスを渡すだけです。
% phpunit EmployeeTest.php [Enter]
# phpunit EmployeeTest.php PHPUnit 3.4.1 by Sebastian Bergmann. EE ←(A) Time: 0 seconds There were 2 errors: 1) EmployeeTest::testSaveObject ←(B) RuntimeException: Not yet implemented. /Users/shigeta/workspaces/ut/Employee.php:58 /Users/shigeta/workspaces/ut/EmployeeTest.php:18 2) EmployeeTest::testSaveObjectAndFetchRow RuntimeException: Not yet implemented. (中略) FAILURES! Tests: 2, Assertions: 0, Errors: 2.
Aの部分では、テストメソッドごとのテスト結果が1文字で表示されます。今回はEという記述がありますが、これはテストがエラーで評価できなかったことを表します。
1文字の表記が1テストメソッドの結果を表しますが、テストメソッドの中で、エラーやアサーションに失敗した場合は、それ以降のアサーションをスキップし、次のテストメソッドを実行します。どのアサーションで失敗したかは、Bの部分に詳細として表示されます。
TDDの流れとしては、次にEmployeeのメソッド内を実装します。テストを実行しつつロジックを組み込み、1つずつテストが通るようにEmployeeクラスを組み上げていきましょう。最終的にEmployeeクラスが完成し、期待どおりの結果を返せばAの部分はドット文字「.」の表記に変わります。
% phpunit EmployeeTest.php [Enter]
PHPUnit 3.4.1 by Sebastian Bergmann. .. ←(A) Time: 0 seconds OK (2 tests, 5 assertions)
逆に、期待していない結果を返した場合、つまりテストが失敗した場合は、Fの表記に変わります(Bの部分)。
% phpunit EmployeeTest.php [Enter]
PHPUnit 3.4.1 by Sebastian Bergmann. FF ←(B)
1つでもFやEの表記があればテストは失敗です。ただちに実装コードを見直し、常にすべてのテストがドット表記になっていなければいけません。
Employeeクラスが完成してもそのテストケースEmployeeTestが不要になるわけではありません。テストはEmployeeクラス自身の修正時や機能追加時に再実行しなければなりませんし、直接関連しないほかのクラスが修正された際にも影響がないかを確認しなければなりません。
特に、重複コードを1つにまとめるリファクタリング作業ではユニットテストの必要性はさらに高まります。例えば、今回は社員テーブルへのデータアクセスクラス「Employee」を作成しましたが、会員テーブルへのデータアクセスクラス「Member」が必要になったときにはEmployeeクラスとの重複部分が発生するでしょう。
この重複は抽象クラス化したり、ユーティリティ的なクラスに書き出さなければなりません。このとき当然Employeeクラス内の修正は発生します。リファクタリングは、あくまでも内部ロジックの最適化であり、引数や返り値については変更しませんので、同じテストがパスするようにリファクタリングを進めれば、メソッドの外面的な仕様と品質を変えることなく最適化が行えます。
特にすでに動いている機能に手を加えるような大規模なリファクタリングは消極的になりがちですが、変更前と変更後のテスト結果が同じであれば、動作に影響がないことを証明できます。
テスト駆動開発は、手間が掛かるなどのイメージが強いですが、テスティングフレームワークはそれらの手間を省くために生み出されたものです。テストメソッドの提供だけでなく、今回紹介したようなクラスの自動生成や、テストの自動実行なども提供されていますし、今後もさらなる自動化が進んでいくでしょう。
Copyright © ITmedia, Inc. All Rights Reserved.