ユニットテストはなぜ必要なの?:PHP開発者のためのテストのすゝめ(1)(2/2 ページ)
開発の全工程の中で、あまり人気がないのがテスト工程だ。ソフトウェアの品質を証明するためのテストは、なぜ低く見られてしまうのか(編集部)
テスト駆動開発
ユニットテストはどのタイミングで実施するのがベストでしょうか。結合テストは、ある程度プログラムができ上がった状態でなければテストのしようがありません。
一方、ユニットテストはどこかのタイミングで一気に実施するものではなく、メソッドを作る前にテストを作り、テストを実施しながら1つのメソッドを作り上げていくのが理想とされています。これをテスト駆動開発やTDD(Test Driven Development)といいます。
例えば、会員データを取得するメソッドをテスト駆動で開発してみましょう。まず仕様ですが、ここでは数値のメンバーIDを渡せば会員情報の連想配列を返すメソッドを作ることにします。メソッド名はMemberTableクラスのgetMember()としましょう。
一般的な開発手法では、仕様が決まった時点で実際にコードを書き始めます。しかし、テスト駆動開発では先にテストケースを作成します。PHPUnitでは以下のようなテストになります。
$memberData = MemberTable::getMember(1); $this->assertType('array', $memberData); // (a)
assertType()メソッドは第2引数が第1引数で指定した型であるかどうかをテストします。(a)の部分ではgetMember()を実行した結果、変数$memberDataがarray型であるかどうかのテストをしています。
しかし、この時点ではgetMember()メソッドがまだ存在しませんのでテストは失敗します。では、MemberTableクラスにgetMember()を実装してみましょう。まずは無条件に空の配列を返すメソッドを作成します。
class MemberTable { static public function getMember($memberId) { return array(); } }
これで初めてテストが通ります。しかし、引数のメンバーIDは無視されており、常に空の配列が返るだけのメソッドですので、完全なテストとはいえません。
では、より完全なテストとするために、取得した連想配列の中身に、最初に渡した引数と同じidが存在するかどうかのテストを追加しましょう。
memberData = MemberTable::getMember(1); $this->assertType('array', $memberData); $this->assertArrayHasKey('id', $memberData); // (b) <-- 追加 $this->assertEquals(1, $memberData['id']); // (c) <-- 追加
(b)では結果配列内にidというキーが存在するかどうかのテスト、(c)ではidキーに対する値が1であるかどうかのテストを記述しました。
この時点ではコードはまだ実装されていないので、再びテストは失敗するでしょう。次はテストが成功するようにコードを実装します。ここで初めてデータベースから取得するなどの実装コードを書くことになります。
また、仕様として、数値以外の値や存在しないメンバーIDを渡したときの例外処理も想定されます。その場合も、数値以外の値を渡したときのテストと存在しないメンバーIDを渡したときのテストを最初に書き、失敗するテストを完成させてからコードを実装します。
このように、テストケース作成→コード実装→テストケース作成→コード実装の工程を少しずつ繰り返し、常に失敗するテストケースの作成を先行してコードを組み立てていくのがテスト駆動開発です。
テスト駆動開発は軽量言語には適さない?
コンパイラ言語の場合、プログラムを変更するたびにビルドという工程が発生しデバッグが非常に面倒な作業となりますので、ユニットテストツールやテスト駆動開発の存在意義が際立ちます。
しかし、PHPなどの軽量言語ではビルド作業自体がなく、コードを修正すれば即座に変更が確認できます。ユニットテストを書かなくても、コードを書きながらWebブラウザで確認した方が早い場合もあるでしょう。このため軽量言語ではテスト駆動開発のメリットを見出せない方も多いのではないでしょうか。
しかし、テスト駆動開発は、ただ単にテストを先に作成することが目的ではありません。重要なのは、クラスやメソッドを設計するためのワークスペースとしてテストケースを使用することです。
どのようなコードを書くにしても、通常は、メモや頭の中である程度仕様を策定すると思いますが、テスト駆動開発ではその仕様策定の試行錯誤をテストケース上に書くのです。つまり、テスト駆動の過程ででき上がったテストケースは、その機能を正確かつ具体的に表現した仕様書そのものになるのです。冒頭で「テストを実施すること、またその過程こそが重要」と述べた理由はここにあるのです。
テスタビリティの高いコードはポテンシャルも高い
前述のとおり、テストありきで開発されたコードは、すでに何度も試運転が行われており、バグと仕様の両方の完成度は高くなっています。それに加えて、再利用性や拡張性、保守性においても優れているといえます。
例えば、ごちゃごちゃしたコードを見やすくするためだけに、その部分だけをメソッドに追いやるようなリファクタリングに心当たりはないでしょうか。そのようなメソッドは引数が多かったり、呼び出しの前提条件や準備処理などの制約が多かったりして、その場所からしか呼び出せない再利用性の低いメソッドになりがちです。
また、準備処理や前提条件が多いということは、それだけの条件を網羅したテストパターンの増加につながりますので、保守性を下げてしまう結果になるのです。
これに対してテスト駆動では、テストケース上で洗練された後にメソッドが実戦配備されます。その洗練の場となるユニットテスト環境には、配備先となるWebアプリケーションフレームワークなどのように豊富な機能は実装されていません。
そのようなシンプルな環境で、テストを先行したメソッド開発をするとなると、どうしてもテストからの呼び出しを意識したメソッド仕様になってしまいます。しかし、この流れこそがテスト駆動開発の一番大きなメリットなのです。
なぜなら、ユニットテストのようなシンプルな環境で育成されたメソッドは、準備処理や前提条件を多く必要とせず、どのような環境からも呼び出せるメソッド、すなわち再利用性の高いメソッドに成長しやすいからです。
テスト駆動開発は保守性だけが論点となりがちですが、特に軽量言語ではこういった再利用性や拡張性を潜在的に持たせるメリットの方がうまみがあるのではないでしょうか。
振舞駆動開発
テスト駆動開発から派生した振舞駆動開発という手法があります。ビヘイビア駆動開発やBehavior Driven Developmentを略してBDDなどと呼ばれることもあります。
テスト駆動開発では、テスト自体の目的の“裏”に「仕様の策定」があると述べましたが、振舞駆動はそれ自体を主目的とします。テスト駆動開発も振舞駆動開発もユニットテストの1つの手段として扱われていますが、一番大きな違いは、前者がメソッドのテストを実施するのに対し、後者はメソッドの仕様(スペック)を記述し実行する点です。テストではなく、こうすれば結果はこうあるべき、という振る舞いをコードで定義するのです。
テスト駆動開発では「これから作るメソッドに対してテストを先に作る」「失敗すると分かっていて先に作る」というところに違和感を覚える方も多いかもしれません。振舞駆動開発では「これから作るメソッドに対して仕様を先に作る」となります。これだと至極まともな流れで抵抗も少ないのではないでしょうか。
PHPではPHPUnitでスペックを記述することができます。また、PHPSpecという振舞駆動開発専用フレームワークもあります。振舞駆動開発については次回以降に詳しく紹介する予定です。
テスト駆動開発にしろ振舞駆動型開発にしろ、コードを書く前にテストやサンプルを書きます。アプリケーションの本筋から離れたコードを書くことに最初は抵抗があるかもしれません。
しかし、「急がば回れ」という言葉があるように、将来のメンテナンス性まで考えると最も確実な近道となる可能性を持っています。本連載がテストを前向きに捉えるきっかけとなれば幸いです。
Copyright © ITmedia, Inc. All Rights Reserved.