Laravel4、依存注入とコンテナ(1)
Tags : Laravel4
IoCコンテナについては既に、いろいろ記事を書いていますが、今回はコンストラクターによる依存の解決を中心に、テストも実際に行いながら、基礎の基礎について学んでいきたいと思います。
初心者のかたは実際に、手を動かし、理解して下さい。そうすれば、簡単に理解できます。
追記:Laravelはコンテナを強要するので、自由が少なくなるような、思わせぶりなツイートがされましたので、簡単に紹介しますが、IoC(DI)コンテナはLaravelオリジナルの機能ではありません。大きなPHPフレームワークも持っていますし、独立したパッケージも存在します。Javaに目を移せば、いろいろな主義があり、それぞれにパッケージが存在します。独自な部分はタイプヒントを利用した依存の解決の部分でしょう。機能のハイライトはタイプヒントにインターフェイスを指定しておき、それに対し具象クラスを指定できるコンテナの能力を結びつけたことです。
Laravel4コンテナのゴールデンルール
- コンストラクターは依存の定義専用(ベストプラクティスの強要)
- インターフェイスの解決には、結合の定義が必須
- コンテナに指示する名前は抽象名
- 抽象名に結合させる具象クラスを定義しなければ、抽象名でインスタンス化を試みてくれる
準備
真新しく、Laravel 4をインストールして下さい。その後、PHPUnitとMockeryをComposerでインストールして下さい。
よくある「できない」ケース
Laravel4標準のコーディングスタイルである、コンストラクターによる依存の指定と自動解決は、「依存注入」というテーマに関わってきます。ですから、依存の注入について、先ず理解しましょう。
この時点で依存とか、注入とか、分からなくても大丈夫です。
さて、今回のチュートリアルで、実際のクラスを作成する場所は、インストールしたディレクトリーに初めから用意されている、app/models
です。Laravelは自由なので、これにこだわることもないのですが、余計な設定を省略するために、モデルのディレクトリーを使用します。
これ以降、特別に指示しませんが、作成するクラスはapp/models
へ、設置して下さい。(ただし、後述のテストクラスは別の場所です。)
先ず、最初のクラスを作成しましょう。りんごです。Apple.php
ファイルを作成して下さい。
<?php class Apple { public function get() { $water = new Water; $sun = new Sun; return $water->get() + $sun->get(); } }
依存注入やユニットテストの解説によくある、「テストできない」ケースです。直接newを使用して、インスタンス化していることで、そう言われます。
ユニットテストとは、日本語で単体テストです。単体とは「テストする対象だけ」と言う意味ですね。つまり、一つのクラスの動作が、正しく動くのかを調べることです。
世の中のクラスが、自分一人だけで動作しているというのは、よほど小さなアプリでもない限り、あり得ません。何かしら、他のクラスと持ちつ、持たれつしています。
今回の、Appleクラスは、Water(水)クラスとSun(太陽)クラスを使用しています。つまり、Appleクラスが正しく動作するためには、WaterとSunクラスに依存しています。一番簡単な言葉で言えば、使っているという意味です。
「単体」テストは自分自身の動作のみをチェックする目的です。ところが、機能上、WaterとSunに依存してしまっているため、その依存をどうにかしないと、Appleクラスの動作だけをテストできません。
なぜなら、WaterやSunクラスの状態が変動してしまうと、Appleクラスはその影響を受けるからです。WaterとSunクラスのget()メソッドが返す値が異なれば、当然Appleクラスのget()が返す値も異なります。WaterやSunクラスが、いつも同じ値を返すのか、違う値を返すのか、Appleクラスには分かりません。そして、大抵のクラスの場合、同じ値を返し続けるクラスメソッドは少なく、多くの場合、異なった値を返すのです。(特定の値だけを返すクラスも作成できます。)
こうした状況で、クラスが返す値を安定させる、昔から取られている手法があります。つまり、テストのためダミーのWaterやSunクラスを作成することです。こうしたテストのためのダミーを「スタブ」と呼びます。
でも、これを実際行うのは、結構手間がかかりますよ。たぶん、Water.phpを一度Water−original.phpとかに名前を変更し、それから、どこかにあるスタブをコピーしてWater.phpにします。同様にSunクラスのスタブもスタンバイしたら、テストプログラムを動作させます。(あなたが賢ければ、テストプログラム内で、自動にコピーするようにしておくでしょう。けど、一般にこれは古いやり方です。こんなやり方をしないで下さい。もしかしたら、使う機会があるかも知れませんので、一応頭の片隅にでも、置いておきましょう。)
面倒ですね。newで作成するインスタンスを置き換えられれば、手間が減らせそうです。テストの時だけ、テスト用のクラスから作成したインスタンス(つまり、一般的には、publicのプロパティーやメソッドから、いつも同じ値を返すインスタンス)に変更できれば、とてもテストが楽になります。
ところが、今回のAppleクラスは、クラス内で、これらの新しいクラスを「生成」しています。そのため、一般的なやり方では、置き換えが「不可能」なわけです。
でも、時代は進化しています。テストツールもPHPも進化しています。ですから、できることには、できちゃいます。実際に試してみましょう。(基礎からではなく、応用から入ります。;P)
早速、このクラスをチェックするテストを書いてみましょう。テストもクラスの一種ですが、こちらはapp/tests
に設置して下さい。
今回の名前は、AppleTest.php
です。
<?php use Mockery as m; class AppleTest extends TestCase { public function testGet() { // overloadを使うのは簡単、 // けど、バッティングを防ぐのが苦労 $waterMock = m::mock( 'overload:Water' ); $waterMock->shouldReceive( 'get' ) ->once() ->withNoArgs() ->andReturn( 50 ); $sunMock = m::mock( 'overload:Sun' ); $sunMock->shouldReceive( 'get' ) ->once() ->withNoArgs() ->andReturn( 50 ); $apple = new Apple; $this->assertEquals( 100, $apple->get() ); } }
Laravel4のユニットテストは、PHPの標準テストフレームワークである、PHPUnitを利用します。テストしやすいように、Laravelが準備を整えてくれています。このテストクラスもPHPUnitを利用し、実行します。
テストする時に、毎度スタブを作成するのは、手間です。そこで、ただ値を返すだけでなく、それ自身にもチェック機能を付けた代替オブジェクトを生成してくれるツールを一般には使用します。Mockeryはそんなツールの一つです。こうしたツールで作成される、ちょっと高機能な代替オブジェクトのことをモックと呼びます。
MockeryでWaterとSunクラスのモックを作成しています。mockメソッドで、モックする対象のクラスを指定します。通常は、この場合のoverload:
を除いた形で指定します。overload:
の部分が、応用編です。説明は後ほど。
一度モックを作成したら、次にモックの動作を指定します。
ただ単に値を返すスタブの役割をするのが、andReturnメソッドです。今回は2つとも整数の50を返すように指示します。
値を返すと言っても、滅多矢鱈に返せば良いのではありません。どのメソッドを呼ばれたら、その値を返してあげるのかを指定しなくてはなりませんね。それがshouldReceiveメソッドです。メソッド名を指定します。
onceメソッドとwithNoArgsメソッドが、スタブにはない「ちょっと高級」な部分です。
onceメソッドで、対象のメソッド、この場合両方共に同じ名前で、getメソッドですが、一回だけ呼び出されることを宣言しています。つまり、全く呼び出されなかったり、2回以上呼び出されると、テストは失敗として扱われます。
withNoArgsメソッドは、対象のメソッド、つまりgetメソッドが、パラメーターなしで呼び出されることを宣言しています。パラメーターを付けて呼び出していたら、テスト失敗になります。
モックが用意できたら、Appleクラスを作成し、getメソッドを呼び出した結果が100であると、確認しています。「WaterとSunクラスのgetを呼び出し、その両方の値を合計した値を返す」機能のテストが書けました。早速、実際にテストしましょう。
インストールディレクトリー、ContainerTutで、以下のコマンドを叩いて下さい。
phpunit
テストが実行され、結果が表示されます。間違っていなければ、最後は緑色で表示され、テストがパスしたことを証明してくれます。
お気づきでしょうか。まだ、Waterクラスも、Sunクラスも実際には作っていません。でも、テストできています。Mockeryが裏でごちゃごちゃ手を回してくれています。
では、overload:
の説明をしましょう。AppleTestクラスのtestGetメソッドを丸々コピーし、testGet2メソッドを追加して下さい。内容は全く同じで構いません。メソッド名だけを変えます。
同じテストが2回実行されます。しかし、同じ内容です。当然、両方共にパスするはずです。やってみましょう。
phpunit
ざんね〜ん!テストは通りません。アイドルばかり聴きすぎたようです。
ええ、本当の理由は違います。本当の犯人は:overload
です。
Overloadは上書きです。実際は上書きするわけでなく、先回りするように指示するのです。
今回のコードやテストに、requireやincludeが使われていないことは、お気づきでしょう。フレームワーク使用する目的の一つは、自動にクラスファイルをロードしてくれる仕組みである「オートロード」も含まれているでしょう。(正確には、Composer上のパッケージはComposerのオートロードを使用するものが多いです。LaravelもComposerと独自のオートローダーを組み合わせて、動作しています。)
オートロードは、クラス名と名前空間により、ある規則に従って、クラスファイルを読み込んでくれる仕組みのことです。今回は名前空間を使いません。ですから今回の場合、クラス名.phpがファイル名になると、単純に考えて下さい。あるクラス名が指定されたら、オートローダーはクラス名.phpファイルを探し、それをrequireするわけです。
そしてMockeryのモックを作成する際に指定するoverload:
は、オートローダーに対し、指定されたクラス名が指定されたら、自動的にMockreyが指定したモックオブジェクトを生成する指定です。簡単に言えば、指定したクラスがnewされたら、Mockeryのモックに置き換わります。
強力ですが、このモックの指定より先に、オートローダーがAppleクラスを読み込んでしまっているのであれば、使えません。
今回、テストの実行で表示されるのは、"Cannot redeclare Mockery_108774385::mockery_init() in /..."です。同じクラスの同じメソッドを定義していると叱られています。これは、テストツールが発生させた失敗と言うより、PHPがエラーを出しています。
最初のoverload:
と、2回目のoverload:
がかち合ってしまっている状態です。
これを回避する方法は、多分、テストの実行方法単位のテストスイーツを分けることでしょう。(なぜ、多分かと言えば、通常私は直接newすることがないので、回避方法を試していません。)
もしくは、@で始まるアノテーション、phpunit.xml、コマンドラインオプションを利用して回避します。テストクラス毎にプロセスを独立させて、動作させます。(まあ、これで回避できることはできるのですが、今度は別のエラーが出てしまい、他の設定をいろいろ変えたりと面倒です。未だ、簡単に回避できる指定方法をLaravel4のテストで見つけていません。Laravel4のせいではありません。Laravel4を活用するなら、もしくはグッドプラクティスに従うなら、コードの中でnewすることを、それをテストすることも、ビジネスロジックでは行わないからです。)
ごちゃごちゃしましたね。まとめましょう。「newをコードの中で使用しても、テストできることはできるが、複数回実行するテストを記述するためには、面倒な手間が必要である。」さらにまとめると、「newを使用すると簡単にはテストできない。」
ですから、コードの中で、直接newを使用するのは避けましょう。
以降のテストのため、追加したtestGet2メソッドは丸ごと削除して下さい。でも、AppleTest.phpとtestGetメソッドは残しておいて下さい。
- Larave4、依存注入とコンテナ(1)
- Larave4、依存注入とコンテナ(2)
- Larave4、依存注入とコンテナ(3)
- Larave4、依存注入とコンテナ(4)
- Larave4、依存注入とコンテナ(5)
- Larave4、依存注入とコンテナ(6)
Laravelでのテストについて、もっと知りたい方は、Laravel Testing Decodedをどうぞ。Laravelの構造や、Laravelで大きなプログラムを作成したい方は、Laravel: 見習いから職人へをどうぞ。