Born Too Late

Yuya's old tech blog.

PHPUnit によるテスト駆動開発 #03 コードを育てる編

2010-07-18 15:26:14

前回のおさらい

前回『#02 テストを書いてみる編』では、「とりあえず、とにかく、テストを書き、PHPUnit で実行すること」という目標のもと、簡単なテストコードの書き方を学びました。

今回の目標

テストコードを拡張しながら、仕様変更に対応する。また、リファクタリングする。

仕様変更

前回は、引数に 2 つの数字を引数に受け取り、その合計値を返すだけの add メソッドを作成しました。

今回は、数字が 2 の場合だけでなく、数字が 3 つの場合も、同じように合計値を返す必要が出てきたと仮定し、話を進めます。

テストコードを拡張する

add メソッドの他に addThreeNumbers メソッドを作ることもできますが、これでは足し算をする際に、数値が 2 つなのか 3 つなのかを意識しないといけないので、ナンセンスです。どちらの場合も add メソッドで処理できるようにしましょう。

前回はは「実際のコード」 -> 「テストコード」という順序でしたが、今度は「テストコード」を先に書いてみます。つまり、「テストファースト」です。

testAddThreeNumbers メソッドを追加しました。

ここで一旦話はそれますが、テストメソッドの名前を test~ とするのは、PHPUnit のルールです。test~ という名前のメソッドだけが、テストとして実行されます。 一応、その他にもテストとして実行させるためのルールはありますが、とりあえず置いておきます。

もし、テストケースの中だけで使うサブルーチン的メソッドが必要であれば、test~ とは違う名前をつければ、そのメソッドはテストとして実行されることはありません。

それでは話を元に戻し、このままテストを実行しましょう。

$ phpunit CalcTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

.F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) CalcTest::testAddThreeNumbers
Failed asserting that <integer:2> matches expected <integer:3>.

/path/to/CalcTest.php:16

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

当然失敗します。Calc オブジェクトの add メソッドはまだ、2 つの数値の足し算にしか対応していません。

一見無駄なように見えますが、この「初めに失敗する」というプロセスも、テスト駆動開発 (Test Driven Development; TDD) においては重要です。そもそものテストコードが間違っていた場合、「常に成功する」という状態になっていることもあり得るからです。

「初めは失敗するが、コードを正しく実装することにより初めて成功する」というプロセスを踏むことによって、より堅実に TDD を行うことができるのです。

コードを育てる

それでは、追加された仕様、そしてテストコードに対応すべく、実際のコードを育ててみましょう。前回のコードは以下のようになっていました。

これを、以下のように書き換えます。

条件分岐により、第 3 引数がセットされていた場合の処理を追加しています。

ここで再びテストを実行します。

$ phpunit CalcTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 4.75Mb

OK (2 tests, 2 assertions)

見事成功しました !

この成功は、今回の「3 つの数値の合計値を返す」が達成できただけではなく、前回作った「2 つの数値の合計値を返す」をも損なうこと無く、コードを育てることができたことを意味します。

テストコードにおける共通処理を一元化する

仕様変更への対応が終わったので、今できているテストコードを振り返ってみましょう。

この記事の冒頭で、testAddThreeNumbers メソッドを作りましたが、「Calc オブジェクト生成」のための記述が、testAdd メソッドと重複していることがわかります。

このような共通処理は、setUp メソッドにより一元化させることができます。

このように、Calc オブジェクトの生成を一元化することで、各テストメソッドで同じことを繰り返す必要がなくなりました。これは DRY (Don't Repeat Yourself; 同じことを繰り返さない) の原則に合致していると言えるでしょう。

この例のような短いコードにおいては、コードの絶対量が増えてしまっていますが、実際の開発において、テストコードはどんどん増えていくものですから、特に理由の無い限りは、setUp メソッドを使って共通化させましょう。

なお、setUp メソッドは、それぞれのテストメソッドが実行される前に、フックされる形で実行されます。なので、testAdd メソッドの $this->calc と、testAddThreeNumbers の $this->calc は、オブジェクトとしては別物ということに注意しましょう。

リファクタリング

さて、今度は実際のコードを振り返ってみましょう。

今回の改修では、if の条件分岐により、引数が 2 つでも 3 つでも合計値を返すよう、処理しています。

しかし今後、4 つの数値を合計も返さないといけなくなってしまったら、どうしましょう。一番簡単なのは、コピペ的に else if で分岐していき、引数が 4 つの場合も同じように処理することです。

しかし、それがまた、5 つ 6 つと増えて行ったら・・・。

この問題を解決するには、add メソッドの処理を根本的に書き換えなくてはなりません。今後のためとはいえ、今動いているものが壊れてしまうかもしれない、というリスクを犯してまで、リファクタリングする必要があるのでしょうか。

この場合、TDD であれば、リファクタリングを取ります。リファクタリングがある程度のリスクを抱える行為だとしても、それを安全に行うことができるのであれば、そして、放置する方が将来より大きなリスクとなることがわかっているのであれば、どちらを選ぶべきかは自明です。

では、今度は、引数がいくつだろうと処理できるよう、add メソッドをリファクタリングしてみましょう。

さっきと同じく、先にテストコードから書きます。

testAddTenNumbers メソッドを書いて、引数が 10 個の場合のテストを追加しました。これを実行すると、やはり失敗します。

$ phpunit CalcTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

..F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) CalcTest::testAddTenNumbers
Failed asserting that <integer:6> matches expected <integer:55>.

/path/to/CalcTest.php:26

FAILURES!
Tests: 3, Assertions: 3, Failures: 1.

今度は、このテストが成功するよう、実際のコードをリファクタリングします。

func_get_arg() を使って、可変引数に対応できるよう、リファクタリングを行いました。再びテストを実行します。

$ phpunit  CalcTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

...

Time: 0 seconds, Memory: 5.00Mb

OK (3 tests, 3 assertions)

見事成功しました !

元のコードを根本的に書き換えていますが、数値が 2 つ、もしくは 3 つの場合の動作も壊すこと無く、引数がいくつ来ても合計値を返すことができるようになりました。

まとめ

今回は、以下のことを学びました。

今回は、前回一旦無視した「テストファースト」というスタイルにのっとり、解説を行いました。

ですが、実際の開発においては、より大きなプログラムを扱うこととなるため、クラス設計がしっかりできる人で無い限り、「テストファースト」の実践は難しいでしょう。

しかし、TDD を実践し続けることで、クラス設計のセンスも向上する、とも言われています。次回はその辺りの、TDD がもたらす副産物について話を進めて行きます。