この記事は TDD Advent Calendar jp: 2011 の 14 日目です.

この記事の概要

  • TDD で開発することで設計上の問題点に気づきやすくなる
  • Singleton はグローバル変数である
  • Singleton の使用はできる限り避けるべきである

テスタビリティを意識しよう

TDD では, 原則としてユニットテストを書いてから実際のコードを実装します.
なので, 自然と「テストのしやすさ (テスタビリティ)」を意識して実装することになります.

そして, TDD においては一般的に, テスタビリティを意識することで, 設計が改善されるとされています.

オブジェクト指向には難しい概念がたくさん登場します.
単一責任の原則 (Single Responsibility Principle) とか, 関心事の分離 (Separation of Concerns)とか, 依存性の注入 (Dependency Injection) とか…
これらを, 書籍などで読むだけで理解できた, という人は少ないのではないでしょうか.
少なくとも私は, TDD を実践することで初めて, これらの原則の言いたいことを実感し, 理解につなげることができたと考えています.

オブジェクト指向設計の大切なことは TDD に学んだ, と言っても過言ではありません.
あまりにもたくさんのことを学んだので, それらをここで全て紹介するのは無理なことです.
なので, 今回はその中から一つだけ, 簡単なものを紹介しようと思います.

この記事では, 明日から意識できるポイントとして, 例えば, Singleton を避ける, ということについて紹介します.

Singleton とは

いわゆる Gang of Four の書籍で紹介されているデザインパターンのひとつで, 以下のようなことを実現するために利用されます.

  • あるクラスのインスタンスが 1 つであることを保証する
  • 常に同一へのインスタンスを取得するためのアクセスポイントを提供する

数あるデザインパターンの中でも比較的理解しやすく, 実装も簡単であるため, これを最初に覚えたという方も多いでしょう.
私自身もそうであったように思います.

Singleton パターンを適用してみる

例えば, PHP においては以下のように実装されます.
ここでは, ありがちな例として, アプリケーション中からグローバルに参照される Config (設定) クラスを作ってみましょう.

要点は以下の 3 点です.

  • コンストラクタ (__construct メソッド) を private にすることで, new を禁止している
  • 代わりに getInstance() という static メソッドでオブジェクトの生成をする
  • getInstance() では初回だけ Config オブジェクトを生成して返すが, 次回以降は最初に作ったオブジェクトをそのまま返す

Singleton をテストする

次に Config オブジェクトをテストするコードを書いてみましょう.
テスティングフレームワークには PHPUnit を使用します.

このふたつのテストはいずれも成功します.

Green

テストは問題無く通った

ですが, 記述する順序を逆にしてみると, いとも簡単に失敗してしまいます.

Red

テストは壊れてしまった

一体, どうしてでしょうか.

Singleton はグローバル変数である

クラスを定義して, クラスメソッドを通してのアクセスを行っていますが, Config クラスのオブジェクトはグローバルに参照可能です.
本来ローカルスコープであるはずのメソッド間を超えて, 状態が共有されてしまいます.
そのため, 一度設定値 foo がセットされると, その後のテストで「foo がセットされていない状態」をテストすることができなくなってしまいました.

このように, Singleton パターンを適用したクラスのオブジェクトは, 本質的にはほとんどグローバル変数なのです.

テストは新鮮なうちに

ユニットテストを書く上で大切なことは, 出来る限りクリーンな状態で始めることです.

グローバル変数, データベース, ファイルシステムなどは, 状態として考えることができます.
コードやテストがこれらの状態に依存してしまうと, 状態の変化に弱くなり, 壊れやすくなります.

Singleton でない通常のクラスであれば, 複数のテストメソッド間で状態を共有されません.
スコープがテストメソッドごとに区切られるので, テスト対象のオブジェクトはガベージコレクションにより, 二度と再利用されないためです.
(もちろん, テストケースクラスのインスタンス変数などに入れてしまえば別ですが)

Singleton では, このガベージコレクションによる状態のクリアが起こらないため, 前のテストコンテキストを引きずったまま, 次のテストを実行することになってしまいます.
複数のテストが影響し合うことによりメンテナンス性が下がるだけでなく, 暗黙的なコンテキストの増大により, 理解もしづらくなります.

さらなる問題

Config 自体はとてもシンプルなので, さほど問題にはならないかもしれません.
ですが, Config::getInstance() がアプリケーションのあちこちで呼び出されていたらどうでしょうか.

例えば, ウェブアプリケーションにおいて, アプリケーションそれ自体を表す Application クラスについて考えてみます.

Application は設定によりデバッグモードの On/Off が切り替えられるとしましょう.
その設定は, Config オブジェクトが保持することになります.

デバッグモードであるかどうかは, Application オブジェクトの isDebug() メソッドで確認できます.
今度はそれをテストするコードを書いてみましょう.

そしてこのテストが通るように Application クラスを実装します.

これで, テストは問題なく通ります.
以下は Config のテストもあわせて実行した結果です.

Green

テストは問題無く通った

ですが, さっきと同じで, このテストも順序を逆にすると失敗してしまいます.

Red

テストはまたしても壊れてしまった

原因は, 先ほど Config のテストが失敗したのと全く同じです.
既に debug という設定値が入ってしまっているため, false を明示的に値をセットしない限りは, デバッグする設定のままになります.

このように, Singleton で実装したクラスそれ自体だけでなく, それを呼び出すクラスにまで, テストのしにくさが伝染してしまう可能性があるのです.

処方箋 1: 状態を初期化できるようにする

Config クラスのテストしにくさは, 状態がクリーンでないことにありました.
そこで, init() メソッドを実装することで, 状態を初期化できるようにしてみます.

テストもこのように書き換えます.

変更点は, Config::getInstance() した後に, init() を呼び出すようにしている点のみです.

これにより, いずれのテストもクリーンな状態でテストが実行されることになったので, 例え順番を変えても失敗しなくなりました.
Application クラスにおいても, 同じアプローチを採ることで, テストの実行順序に依存することは無くなります.

処方箋 2: 依存性の注入 (Dependency Injection) の利用

とはいえ, これで問題が無くなったわけではありません.
この Application クラスには, Config というクラス名がハードコードされており, 2 つのクラスは密結合になってしまっています.

これでは, Config に似た動きをした別のクラス (例えば CachedConfig とか) を作っても, 差し替えるには全ての Config を書き換える必要が出てしまいます.

ではどうするか.
オブジェクト指向が本来持つモジュール性を活かすのであれば, 引数で Config オブジェクトを渡すことを検討しましょう.
ここでいう引数とは, コンストラクタ引数でも, メソッド引数のどちらでも構いません.

先ほどよりもややコードが増えてしまいましたが, Config クラスのハードコードが無くなりました.
Application と Config の間の結合は緩くなり, CachedConfig や YamlConfig など, 同じように振る舞う別クラスへの差し替えが容易になりました.
また, モックスタブを使ってのユニットテストも書きやすくなっています.

このように, 依存するオブジェクトのクラス名をハードコードするのではなく, 引数で渡すことを依存性の注入 (Dependency Injection) といいます.

ところで, クラス名のハードコードが問題になるのは, 何も Singleton だけの問題ではありません.
例え Singleton ではなくとも, new するクラス名をハードコードすれば, 同じようなことが問題になります.

とはいえ, Singleton を利用すると, getInstance() メソッドを利用した依存にしてしまいがちです.
出来る限り依存性の注入を利用し, クラス名をハードコードする箇所を最小限に留めることで, 後の改修時のコストを大幅に下げることができます.

処方箋 3: そのクラスは本当に Singleton なのか

あなたが今実装しようとしているクラスは, 本当に唯一なのでしょうか.
また, 通常唯一であるとしても, コンストラクタを private にしないと実現できないことなのでしょうか.

確かに, Singleton の「常に同一のインスタンスを返す」という特性は便利です.
ですが, 少なくとも私の経験においては, 「インスタンスが 1 つであることの保証」は, そんなに気張るほどのことでは無かったのではないか, と考えてしまいます.

また, 見方を変えれば, Singleton は, そのクラス本来の役割 (Config であれば設定の保持) とは別にもうひとつ, 「オブジェクト生成の管理」という, 全く違った責任を持ってしまっています.
これは, 単一責任の原則に違反していると言えるでしょう.

これをどう解決するか.
少々場当たり的ですが, 例えば Config と, その生成を責任とする ConfigManager に分割する, といったことが考えられます.

この実装自体はあまりオススメしませんが, とりあえず設定の保持 (Config) という責任と, 設定オブジェクト生成の管理 (ConfigManager) という責任を, 個別のクラスに分割することができました.
これにより, Config クラスのコンストラクタを private にするという縛りは無くとも, ConfigManager を利用する限りにおいては, 常に同一のインスタンスを得ることができるようになりました.

コンストラクタが使用できる以上は, オブジェクトの生成は自由になったので, テスト時は毎回オブジェクトを生成・破棄し, クリーンな状態でのテストが可能になりました.
もう init() は必要ありません.

テストメソッドごとにコンテキストが分割されたので, ガベージコレクション任せで安心してテストに集中することができるようになりました.

まとめ

Singleton の話ばかりしましたが, この記事で私が言いたいのは, TDD で開発することで設計上の問題点に気づきやすくなる, ということです.

TDD の実践から学べることは多いにあるので, まだやったことないという方には, まず趣味の開発でいいので, 軽い気持ちでやってみることをオススメします.
業務に TDD 自体を導入する前の段階でも, 設計について新たな目線を得ることができるので, 何かしら良い効果を得ることでしょう.

, , ,

皆さん, ユニットテスト書いてますか.

TDD (テスト駆動開発) によるプログラミングは本当に楽しいものですが, コマンドをいちいち手動で実行するのは面倒ですよね.
テストを自動化しているんだから, その実行も自動化したいですよね.

この記事では, 私が仕事や趣味で使っている PHPUnit を例に, テストの実行の自動化について紹介します.
PHPUnit の, としてはいますが, 他の言語で使えるテクニックもあります.

なお, ここでの自動化は開発しながらの自動実行のことで, CI (継続的インテグレーション) の話は出てきません.

その前に…

私の開発時のターミナルは以下のようになっています.

開発時のターミナル

開発時のターミナル

GNU screen での画面分割を利用して, 左半分をソースコードに, 右上をテストコードに, 右下をテストの実行に使っています.
これは作業の途中でいろいろと変動しますが, 右下ではほぼ常にテストが自動実行されるようになっています.
(ターミナルの透明化を利用してバックグラウンドでニコ動を観ることもできますね)

この小さい画面での実行を前提に, 紹介していきます.

シェルスクリプトを利用する

まずはとにかく, 決められたコマンドを定期的に実行しましょう.

コマンドを定期的に実行, というと watch コマンドを思い浮かべる人が多いと思いますが, これは使いません.
何故か, ターミナル上の色が無効化されてしまい, レッドやグリーンがわかりづらいからです.

色の使える watch は無いか, と探した所, superuser という Q&A サイトにちょうどいいものがありました.
bash watch command with colors preserved

ここへの書き込みを参考に, 以下のようなシェルスクリプトを用意しました.

適当な場所に保存して, chmod +x して実行権限を与えたら, 以下のように使用することができます.

$ ./watch2 phpunit --colors

これで, 出力の色づけを維持したまま, PHPUnit を 2 秒間のインターバルで起動し続けることができるようになりました.

プロジェクトの規模によっては全てのテストを実行するのに時間がかかることもあるので, そういうときは特定のファイルを指定しましょう.

$ ./watch2 phpunit --colors tests/Foo/BarTest.php

今自分が作業しているファイルだけを指定することで, フィードバックを素早く的確に得られるようになります.
コマンドの指定は手動ですが, 環境の用意はとても楽なのでよしとしましょう.

Stagehand_TestRunner を利用する

Stagehand_TestRunner は, PHPUnit だけでなく Lime や PHPSpec にも対応したテストランナーです.
(2011-08-16 14:00 追記: Lime への対応は勘違いでした. @heavenshell さんご指摘ありがとうございました.)

インストールについては本家の Wiki を参照しましょう.
テスト駆動開発のためのテストランナー

PHPUnit 本家よりも優れた表示が特徴です.

Stagehand_TestRunner による実行結果

Stagehand_TestRunner による実行結果

この出力形式は testdox と呼ばれるもので, テストメソッド名を英文っぽく表示してくれます.
testdox を表示する機能自体は PHPUnit にもあります (--testdox オプション) が, Stagehand_TestRunner を使うと色づけされるので, よりわかりやすくなります.

Stagehand_TestRunner には他にも様々な機能がありますが, ここで紹介するのはディレクトリの監視によるテストの自動実行です.

これを phpunit.xml として保存して, 同じディレクトリ上で以下のコマンドを実行します.

$ phpunitrunner --phpunit-config=phpunit.xml -cRa

c が色づけ, R が指定ディレクトリ (phpunit.xml で指定していますね) 以下を再帰的に, a が自動実行のためのオプションです.

これで, ディレクトリに変更のあったときだけテストが実行されるようになりました.

実行対象のテストを正規表現で絞り込んだりすることもできますが, それらの詳細については以下の Wiki が参考になります.
Stagehand_TestRunner ユーザーガイド

watchr を利用する

最後に紹介するのは, 最近一番よく使っている方法です.
watchr はファイルの変更を監視するツールで, 検出時にあらゆる処理をフックさせることができます.

watchr は Ruby 製のツールなので, 処理は Ruby で書く必要があります.
といっても, 簡単な正規表現が書ければ充分なので, Ruby 未経験でも問題無いでしょう.

インストールについては Rubygems をインストールして gem install watchr するだけです.

watchr の設定ファイルとして, 以下のようなものを用意します.

これは src ディレクトリのソースコードの変更時に対応するテストを実行すること, テストコードの変更時にはそのファイル自身のテストを実行すること, を設定しています.

そして以下のようなコマンドを実行することで, 監視状態に入ります.

$ watchr phpunitrunner.watchr

前提として, ソースコードが src/Foo.php であればテストコードは tests/FooTest.php というように, 命名規則にそったファイル名になっていることが必要です.

変更のあったときにだけ, そのファイルだけテストが実行されるので, 素早く確実にフィードバックを得ることができます.

また, Ctrl + \ を押すことで, 全てのテストを実行することもできます.

注意としては, 監視対象のファイルの読み込みが watchr の起動時なので, 例えば途中で新規に追加したファイルは監視対象に入っていないことです.
ある程度開発が進んで, 作業のメインが既存ファイルの編集になったときに, より高い効果を発揮するでしょう.

まとめ

テストだけでなく, その実行まで自動化することで開発にリズムが生まれ, フロー状態に入りやすくなります.
開発効率を上げ, 楽しい TDD ライフを送りましょう.

その他, オススメのツールややり方などありましたら, 是非 @yuya_takeyama まで教えてください.

, , , ,

前回のおさらい

前回『#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 がもたらす副産物について話を進めて行きます。

, ,