Laravel4、依存注入とコンテナ(2)

タグ: Laravel4  

コードの中でnewを使うと、ユニットテストする時点で面倒なことになるのは、いくらか理解してもらえたでしょう。

ですから、回避しましょう。

Orangeクラスで試しましょう。Orange.phpファイルです。

<?php

class Orange
{

    public function __construct( $water, $sun )
    {
        $this->water = $water;
        $this->sun = $sun;
    }

    public function get()
    {
        return $this->water->get() + $this->sun->get();
    }

}

見て下さい。自分でnewするのは止めました。クラスのコンストラクターでインスタンスを受け取ることにしました。

Appleクラスでは、自分の内部で動作に必要な他のクラスをnewしていましたが、Orangeクラスでは、自前で用意しません。Orangeクラスを作成してもらう時に、呼び出し側で用意してもらうように変更しました。

これが、「依存注入」です。「動作させるのに依存している、必要なクラスを外部から注入して(与えて)もらう」ことです。外部から受け取る方法はいくつかありますが、コンストラクターで受け取るほうがメリットが大きいと考える人が多いらしく、主流になっています。

テストコードをご覧ください。OrangeTest.phpです。

<?php

use Mockery as m;

class OrangeTest extends TestCase
{

    public function testGet()
    {
        // overloadじゃない、普通のモックオブジェクト

        $waterMock = m::mock( 'Water' );
        $waterMock->shouldReceive( 'get' )
            ->once()
            ->withNoArgs()
            ->andReturn( 50 );

        $sunMock = m::mock( 'Sun' );
        $sunMock->shouldReceive( 'get' )
            ->once()
            ->withNoArgs()
            ->andReturn( 50 );

        // そのクラスで使用するインスタンスを
        // 外部から与えられるようになる。
        // これをもったいぶって「注入」と言う。
        $orange = new Orange($waterMock, $sunMock);

        $this->assertEquals( 100, $orange->get() );
    }

}

AppleTestとの違いは、モックを生成する時に、overload:の指定が無いだけです。ああ、もちろんAppleクラスの代わりに、Orangeクラスをテストしていますよ。:D

さて、テストを実行してみましょう。

phpunit

ざんねーん。失敗しました。

エラーをよく見ると、先程のエラーと似ています。何が起きたのでしょう。

PHPUnitはphpunit.xmlで指定された、テスト先のディレクトリー下をチェックし、Test.phpで終わるファイルをテスト対象として、ユニットテストを実行します。絶対ではありませんが、こうした仕組みを採用している場合、同じディレクトリー中に存在するファイルは、一般的にABC順で実行されるものです。

AppleTest.phpとOrangeTest.phpでは、多分AppleTest.phpが先に実行されます。

ApppleTestと言えば、overload:指定です。実は、これが問題です。

Laravel4が用意しているphpunit.xmlでは、各テストクラスのテストを別々のプロセスとして実行しないように指定してあります。ユニットテストでは、テストスピードも重要であり、クラスを別プロセスで動作させると、目に見えて遅くなるからです。ちなみに、PHPUnitのデフォルトでもあります。

簡単に言えば、基本的に同じ環境で、各テストクラスを実行しています。そのため、AppleTestで使用した、overload:の影響が、それ以降に行うテストに影響を与えるのです。

各テストクラスを別々に実行するようにすると、実行速度が大幅に遅くなります。そして、今度は別のエラー(Notice)が発生して実行が止まります。これを回避するには、エラーレベルの設定を変更して…、止めときましょう。

そもそも、newを使用したのが原因です。使わなきゃ良いだけです。そのため、いろいろ設定したり、テストの実行時間を遅くする必要はありません。

この失敗の手間をかけたのは、再度「newはテストできるんだけど、できるだけ避ける」という考えを持ってもらうためです。Mockeryでoverload:を使用した時の、副作用を認識してもらうためでもあります。

では、AppleTest.phpを削除しましょう。そしてphpunitを実行しましょう。

うまく動作します。どうぞ、AppleTestでも行ったように、OrangeTestでもtestGetメソッドを丸々コピーして、testGet2メソッドを作成して下さい。そして、phpunitを再実行です。

問題なく動きます。overload:指定時は、いろいろ苦労しましたが、使わなきゃ、テストなんて簡単です。いくらでも、モックが使用できます。

さて、テストも無事通過しました。気分もいいですし、Orange.phpを見なおしてみましょう。

そうすると、ふとした疑問が湧いてきます。

「この、Orangeクラスを使用するクラスはどうなるんだ?」

一例を見てみましょう。

<?php

class Fruit
{

    public function getOrange()
    {
        // こうすると今度は、このクラスと
        // このメソッドがテスト困難に…

        $orange = new Orange(
            new Water, new Sun
        );

        return $orange->get();
    }

}

ご察しの通り、上のクラスでnewしなくてはなりません。そうすると、今度はFruitクラスがテストできなくなります。

これを避けるために、必要なクラスをそれらを使用するクラスで生成するようにするには…、すると、一番上のクラスが巨大なnewのお化けになってしまいます。その、アプリケーションで使用する全クラスのオブジェクトをインスタンス化するために、newを指定するのです。

これは、実用的ではありません。そこで、ちょっと改造しましょう。今度はBanana.phpファイルです。

<?php

class Banana
{

    public function __construct( $water, $sun )
    {
        $this->water = $water ? : new Water;
        $this->sun = $sun ? : new Sun;
    }

    public function get()
    {
        return $this->water->get() + $this->sun->get();
    }

}

コンストラクターに引数が指定されないと、$waterと$sunはnullです。ですから、三項演算子を使用し、パラメーターが指定されない時は、対象のインスタンスをnewし、渡されときは、渡されたインスタンスを利用するようにしています。

特別な、仕組みを利用せず、コンストラクターにより依存を注入しようとするなら、PHPでは標準的な形式になります。

テストは、OrangeTest.phpと同じ形式で行えます。BananaTest.phpは自分で作成して下さい。(宿題です。)

実際、Bananaクラスを使用する時は、シンプルにnewします。

<?php

class Fruite
{

    public function getOrange()
    {
        // こうすると今度は、このクラスと
        // このメソッドがテスト困難に…

        $orange = new Orange(
            new Water, new Sun
        );

        return $orange->get();
    }

    public function getBanana()
    {
        // 実際に使用する時は、引数を
        // 省略して呼び出す。

        $banana = new Banana;

        return $banana->get();
    }
}

やりました。これでBananaのテストはバッチリです。実用時も問題ありません…。

あれ?でも、また、new使っちゃいましたね。これでは、このFruitクラスのテストが…、ざんねーん!

アプリケーション全体を簡単にテストできるようにするには、手間がかかりますね。(実際には、このFruitクラスでも、コンストラクターで渡されたパラメーターをチェックし、渡されていない場合(NULL値)にインスタンスを生成するコードで対応できます。IoCコンテナは、依存注入を採用するために、必ず使用しなくてはならないものではありません。)

Laravelでのテストについて、もっと知りたい方は、Laravel Testing Decodedをどうぞ。Laravelの構造や、Laravelで大きなプログラムを作成したい方は、[Laravel: 見習いから職人へ]Laravel: 見習いから職人へをどうぞ。