単体テストとテスト駆動開発(PHPUnitの例)
2016-07-01 19:39
こんにちわ。開発担当の野澤です。
今日は弊社でも最近になってようやく導入した”PHPUnit”と、
システム開発におけるテストの役割や意義について簡単にまとめたいと思います。
今回はどちらかというと、プログラマー初級者向けの方や、
非技術者の方(ディレクターや経営者の方)でも
プログラムのテストの最近の潮流や概念がなんとなく掴めるように書いております。
まずはPHPUnitの概要をおさらいしたあとに、テスト駆動開発の考え方について見ていきたいと思います。
PHPUnitとは
wikipediaによると「PHPの単体テストのフレームワーク」だそうです。
Javaの単体テストツールであるJUnitを祖とする
xUnitのPHP版で2014年に初めてリリースされたそうです。
そもそも単体テストとは、ソフトウェア構成する個々のパーツが
正しく動いているかどうかを検証するテストのことですが、
xUnitなどのテストツールでは、検証するためのプログラムを書いて
自動実行をすることができます。
特にオブジェクト指向プログラミングにおいては
ソフトウェアはプログラムのまとまりであるクラスによって
構成されるため、単体テストはこのクラスの振る舞いが
正しいものかを検証していくことになります。
テストコードの例
例えばX件の記事を1ページあたりN件ずつ表示した場合
全部で何ページになるか、といったことを算出するプログラムがあったとします。
1 2 3 4 5 6 7 |
class Hoge { static function countPage($total, $per_page) { return $total / $per_page; } } |
単体テストをしない場合は、手作業で$totalや$per_pageのパターンを作成して
var_dump()を使って結果が正しいかどうかを目視でチェックしたりしていました。
PHPUnitでのテストコードは下記のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
require dirname(<strong>FILE</strong>).'/Hoge.php'; class HogeTest extends PHPUnit_Framework_TestCase { /** * @dataProvider provideSample */ function testCountPage($total, $per_page,$expected_page) { $page = Hoge::countPage($total,$per_page); $this->assertEquals($page,$expected_page); } function provideSample() { return array( array(10,5, 2), array(20,5, 4), ); } } |
コメントアウトされているなかの@はアノテーションと呼ばれ
PHPUnitでは特別な意味合いを持ちます。
@dataProviderはテストのために、データを提供する
メソッドを指定するためのものです。
provideSampleでは2パターンのデータをtestCountPageメソッドに提供します。
まずは全部で10件の記事で、1ページあたり5件表示する場合であれば、ページ数は2ページになることが期待できます。
countPage()メソッドに上記のデータを引き渡して計算したページ数と、
期待するページ数が一致しているかどうかをassertEqualsメソッドによって
検証しています。
これが一致していなければ、テストは失敗となります。
テストパターンが100以上になったりするとvar_dumpで検証するのは
とても気力と体力がいりますし、見落としなども発生する可能性がありますが
こうしておけば、テストしたいデータがあれば配列に追加するだけで済みますし、
var_dumpして目視で結果を確認しなくても、テストが成功したかどうかは
テスト結果を見るだけで十分です。
また、プログラムになっているので、テスト対象プログラムを改修しても
同じテストをすぐに繰り返し実行することができます。
テストコードを使わない場合、また100パターンの結果を目視でチェックするのは結構しんどいですよね。
また、先ほどのプログラムには一部問題があります。
先ほどのデータプロバイダに2行、新たなテストデータを入れてみます。
1 2 3 4 5 6 7 8 |
function provideSample(){ return array( array(10,5, 2), array(20,5, 4), array(20,'hoge',false), array(20,0,false), ); } |
本来数字で割り算をするはずなのですが、”hoge”という文字列を
与えてみた場合はどうでしょうか?
当然割り算ができずエラーとなります。
phpunitを実行すると下記のようにどのテストでどんなエラーが出たかをレポートしてくれます。
1 2 3 4 5 6 7 8 9 10 11 |
19:07:26 nozawa$ phpunit --color --stop-on-failure HogeTest PHPUnit 5.3.2 by Sebastian Bergmann and contributors. ....E Time: 117 ms, Memory: 12.00Mb There was 1 error: 1) HogeTest::testCountPage2 with data set #2 (20, 'hoge', false) Division by zero /Users/nozawa/Downloads/blogs/Hoge.php:6 /Users/nozawa/Downloads/blogs/HogeTest.php:27 FAILURES! Tests: 5, Assertions: 4, Errors: 1. |
また同様に割る数を数字のゼロにした場合はどうでしょうか?
PHPの場合、分母がゼロだとwarningが発生します。
実際にシステム上で分母がゼロ、つまり一ページあたりに何も表示しなかった場合の
全体のページ数がいくつかを算出することなんてないと思いますし、
まさか割る数が入るべきところに”hoge”なんて文字列がくるはずもありません。
でも、システムでは何が起こるかわからないのです。
もし何かの間違いでゼロで割り算を行ってしまった場合、
処理がそこでストップしてしまいます。
単体テストはそのリスクの芽を事前に摘んでおくことが目的の一つでもあります。
先ほどのプログラムを改善するとこうなります。
1 2 3 4 5 6 7 8 9 10 11 |
static function countPage2($total, $per_page) { if(!is_numeric($total) || !is_numeric($per_page)){ throw new InvalidArgumentException('数字じゃないと割り算はできませんよ'); } if($per_page == 0){ throw new InvalidArgumentException('割る数が0だと割り算はできませんよ'); } return $total / $per_page; } |
数字じゃない数を計算しようとしてたり、分母が0だった場合は
例外を投げるようにしました。
こうしておけば、処理は止まらずに済みますし、この処理を実行している
プログラムには、問題が発生した場合にはどうするかを組み込んでおいてもらえればOKです。
こんなふうに、テストツールを使ったりテストコードを書いていけば、
自然と、「ここに数字を与えたらどうなるだろう」とか「文字列を与えたらどうなるだろう」という
想像力が働きます。
実際、プログラムを作成している場合は、どうしてもイメージしているものが
表現できるかどうかに注力してしまいがちで、
うまくいかなかった場合(「異常系」といいいます)のことを想像しながら書くことは難しいです。
プログラムが完成してからバグが発生して、異常系があることに気づくことも多いのではないでしょうか。
テストを前提にプログラムを組んでおけば、こうしたバグを極力減らしていくことができます。
これを「テスト駆動開発」と言います。
テスト駆動開発
近年の開発現場では「テストファースト」や「テスト駆動開発」といった
開発の方法論を採用するところが増えてきているといいます。
この考え方は、個々のプログラムを作成する際に、どういう状態にあるべきかに基づいて
テストコードを書き、それをクリアするようにプログラムを書いていくことによって、
プログラムの品質を高く保てることを目的としています。
このようにプログラムをスクラップ&ビルドしていくプロセスを
「リファクタリング」といい、品質の高いプログラムを作成するためには
必要なステップだと考えられています。
システムを個々のパーツによって構成されるようにしておけば、
システムの仕様が変わったとしても、全体を変更する必要はなく、
必要なパーツを変更するだけで済みます。
また、テストコードも一部を修正すればまた再利用ができます。
「スクラム」や「XP」と呼ばれるアジャイル開発手法においては
このテストファーストが重要視されており、弊社でもその発想に基づいて
試行錯誤しているところです。
ただし、広義の意味の「テスト」として、
できあがったシステムや画面が妥当なのかをチェックすることも
テストであり、それを繰り返し行って製品全体の品質をあげていくことは
プログラムの品質以上に重要なことです。
バグのない完璧なシステムがあったとしても、
使い勝手が最悪だったら意味がありません。
逆に多少のバグがあったとしても、きちんと求めていることが
実現できていたら、使い手は満足できるはずです。
近年はこうしたユーザー価値を高めるために、
「振る舞い駆動開発」と呼ばれる開発手法が考案され
導入している企業も多いようです。
なので精微な単体テストそのものを完成させることを
目的とせず、システムや製品を俯瞰して見ていくことが大事ですね。
次回はもうちょっと技術者向けに
具体的なPHPUnitの使い方を紹介していければと思います。