前回のおさらい

前回『#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 つの場合の動作も壊すこと無く、引数がいくつ来ても合計値を返すことができるようになりました。

まとめ

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

  • 「テストコードを書く」 -> 「テスト実行 (失敗)」 -> 「実際のコードを書く」 -> 「テスト実行 (成功)」
    というサイクルにより、着実にコードを育て、リファクタリングすることができる。
  • まず「失敗する」ことを確認してから実装を行うことで、テストコード自体のミスを防ぐことができる。
  • テストケースにおいて、テストメソッドは test~ と命名する必要がある。
  • setUp メソッドを使うことで、テスト中の共通処理を一元化することができる。

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

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

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

, ,

目的

とりあえず、とにかく、テストを書き、PHPUnit で実行すること。

想定される読者

  • PHP5 の基本的な文法を理解している
  • テスト駆動開発 (Test Driven Development; TDD) という言葉は知っているが、やってみたことは無い
  • PHPUnit 3.4 以降をインストールしている

TDD を知らない、PHPUnit をまだインストールしていない、という方は前回の記事『#01 インストール編』をご覧ください。

逆に、上記項目より上を行く方にとっては、特に参考になることは無いかもしれません。

テストされるコード

初めに、実際にプログラム上で使われるコードを用意します。

TDD の世界には「テストファースト」という言葉があります。それは、「実際のコードよりもテストコードを先に書く」というスタイルを表していますが、ここではそういった細かいことは気にせず話を進めます。

2 つの引数を足し算するだけの簡単なメソッドを持った、非常にシンプルなクラスです。

テストするコード

以下のようなコードを用意します。

これが、PHPUnit でテストを書く際の最小単位である、テストケースです。

注目すべきは、testAdd メソッドの中身です。これは、assertEquals というメソッドが、「$calc オブジェクトの add メソッドに、引数として 1 と 1 を渡せば、2 になるよね」といったことを確かめるためのコードです。

その他の部分は、PHPUnit でテストケースを書くためのおまじないみたいなものです。細かいルールについては次回以降、徐々に説明するとして、とりあえず次に進みます。

テストを実行する

ターミナル上で以下のコマンドを実行します。

$ phpunit CalcTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

.

Time: 1 second, Memory: 4.75Mb

OK (1 test, 1 assertion)

最後の行の「OK」は、このテストが成功し、コード (Calc.php) が正確に実装されていることを示しています。また、1 つのテスト (1 test) と 1 つのアサーション (1 assertion) が実行された、ということも示されています。

もし add メソッドに間違いがあった場合は、PHPUnit の実行結果は以下のようになります。

$ phpunit CalcTest.php
PHPUnit 3.4.15 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

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

/path/to/CalcTest.php:10

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

これは、add メソッドが 2 を返すはずなのに、実際は 1 が返ってきてしまったために、テストが失敗してしまったことを示しています。

このような場合は、テストが成功するまでコードを修正しましょう。

アサーションとは

アサーションとは、変数の値や、メソッドの返り値をチェックすることです。さっきのテストケースでいえば、assertEquals メソッドがそれを行っています。

ざっくり言ってしまえば、前回からの繰り返しになりますが、「このメソッドにこの引数を与えると、この値が返ってくるよね」といったことをチェックすること、と言えるでしょう。

アサーションには多くの種類があり、例を挙げれば、

  • assertEquals()
    2 つの値が等しいか
  • assertTrue()
    値が true であるか
  • assertFalse()
    値が false であるか
  • assertType()
    値の型が正しいか

などなど、たくさんあります。

ですが、とりあえずは assertEquals と assertTrue だけ使えれば大抵のことは何とかなるので、他は必要に応じて調べながら覚えていけばよいでしょう。

まとめ

今回は、テストを書くこと、そして実行すること、に的を絞って解説しました。

次回は、テストコードを拡張させながら、PHPUnit でテストを書く際のルールについて、もう少し踏み込んでいこうとおもいます。

, ,

テスト駆動開発とは

テスト駆動開発 (Test Driven Development; TDD) とは、テストケースを書くことからプログラミングを始める、開発手法です。

ざっくり言うと、「このメソッドにこの引数を与えると、この値が返ってくるよね」といったことを、テストコードを用いて検証しながら、開発を進めていくスタイルと言うことができるでしょう。

TDD を知らない人がこの説明を聞くと、「TDD はコードの安全性のための手法である」と思われるかもしれません。
確かに、TDD はコードの安全性をもたらします。
しかし、TDD がもたらすものの中には、「コードの簡潔性」をも含まれます。

何故コードが簡潔になるのか

これは私見ですが、簡潔で読みやすいコードは、「入力と出力の束である」ことが多いです。
それはつまり、メソッド・関数が適切な単位で細かく文節され、それらの協調により組み立てられているコードです。

逆に読みづらい・わかりづらいコードは、「1 つのメソッド・関数が頑張り過ぎている」コードです。
1 つのメソッドが長いと、開発やデバッグの際に、「コード中のどこにバグが潜んでいるのか」を見つけ出すことが困難になります。

TDD による開発では、「テストしやすいコードを書く」ために、必然的に、コードが細分化されます。
その 1 つ 1 つに対して「このメソッドにこの引数を与えると、この値が返ってくるよね」を書いておけば、大幅なリファクタリングも、コードの同質性を検証しながら行うことができます。

TDD と私

新卒で Web エンジニアとして就職した 1 年前、私は TDD を知りませんでした。

そのうち、ブログなどで TDD について知り、Ruby の標準ライブラリ test/unit を使って、遊び程度にテストを書くようになります。

その後、東京 RubyKaigi 03 における、和田卓人氏 (@twada) のワークショップで RSpec の写経を行うことで、TDD の威力を実感することになります。

それ以来、仕事でも、PHPUnit を用いての、TDD による開発を心がけています。

プログラミングについては少しずつ勉強していますが、その中でも一番インパクトがあったのは間違いなく TDD であったと言いきることができます。
とはいえ、まだまったくの初心者レベルなので、勉強しながら、このブログで発表していこうと思います。

PHP Unit をインストール

Ubuntu 10.04 上の LAMP 環境で、PHP5 以上 (筆者は 5.3) 、そして pear がインストールされていることを前提としています。

公式ドキュメントに倣いますが、root 権限が必要となるため、sudo 付きで実行します。

sudo pear channel-discover pear.phpunit.de
sudo pear channel-discover pear.symfony-project.com

上記コマンドで PEAR チャネル (ライブラリのインストール元のサーバ) を追加し、次のコマンドで PHPUnit がインストールされます。

sudo pear install phpunit/PHPUnit

これでインストール完了です。引数無しで phpunit コマンドを実行して、バージョン情報と Usage が表示されれば成功です。
2010 年 7 月 11 日現在では、PHPUnit 3.4.14 がインストールされます。

本日はここまで。
次回は実際にテストコードを書いていく予定です。

,