Laravel3、リポジトリークラスのテスト

タグ: Laravel3   テスト志向  

この記事は、Laravel3でのテストについてのシリーズです。

テストの対象は、以下のシリーズで作成した、りんごテーブルに対する簡単なCRUD操作プログラムに対するものです。

リポジトリークラス

リポジトリークラスは直接Eloquentを利用しないための中間クラスです。

AppleRepoクラスを復習しましょう。

<?php

namespace ProjectH\Repositories;

use IoC;
use ProjectH\Exceptions\ExecutionErrorExceptions\IdNotFoundException;
use ProjectH\Exceptions\ProgramErrorExceptions\ValidatorClassNotFoundException;

class AppleRepo
{
    private $apple;

    public function __construct()
    {
        $this->apple = IoC::resolve( 'Apple' );
    }

    public function create( $data )
    {
        if ( IoC::registered( 'AppleValidator' ) )
        {
            $appleValidator = IoC::resolve( 'AppleValidator' );
            $appleValidator->validate_create( $data );
        }
        else
        {
            throw new ValidatorClassNotFoundException(
            'Appleモデルのバリデタークラスが存在していません。' );
        }

        $apple = $this->apple->create( $data );

        $apple->save();
    }

    public function read( $id )
    {
        $apple = $this->find( $id );

        return $apple;
    }

    public function update( $id, $data )
    {
        $apple = $this->find( $id );

        if ( IoC::registered( 'AppleValidator' ) )
        {
            $appleValidator = IoC::resolve( 'AppleValidator' );
            $appleValidator->validate_update( $id, $data );
        }
        else
        {
            throw new ValidatorClassNotFoundException(
            'Appleモデルのバリデタークラスが存在していません。' );
        }

        $apple->fill( $data );

        $apple->save();
    }

    public function delete( $id )
    {
        $apple = $this->find( $id );

        $apple->delete();
    }

    private function find( $id )
    {
        $apple = $this->apple->find( $id );

        if ( $apple === null )
        {
            throw new IdNotFoundException( 'IDが見つかりません。' );
        }

        return $apple;
    }

}

リポクラスが使用するクラスは二つあります。ElqouentモデルのAppleクラスとAppleValidatorクラスです。両者ともIoCコンテナ経由で取得するようにしています。

もしEloquentモデルを直接使用すると、リポクラスの動作確認のため、毎回テーブルを使用しなくてはなりません。バリデーションクラスを直接ハードコードしてしまえば、リポクラスの単体テストを行うため、バリデーションの内容まで考慮する必要が起きます。両クラスともリポクラスの単体テスト時は、簡単なスタブに置き換え、単体テストを通します。

テストコード

<?php

class AppleRepoTest extends PHPUnit_Framework_TestCase
{

    public function testSetAppleClassPropertyInConstructor()
    {
        // 時間に関するフィールドを含まない空のEloquentりんごスタブ
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleEloquentStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $ref = new ReflectionClass( $appleRepo );
        $prop = $ref->getProperty( 'apple' );
        $prop->setAccessible( true );

        $apple = IoC::resolve( 'Apple' );

        // このテストで取得したAppleクラスのインスタンスと、
        // コンストラクタで取得されるAppleクラスのインスタンスの比較
        $this->assertEquals( $apple, $prop->getValue( $appleRepo ) );
    }

    public function testCreateNormalTerminal()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );
        IoC::register( 'AppleValidator', '\\TestStubs\\AppleRepo\\AppleValidatorPassStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $appleRepo->create( array ( ) );
    }

    public function testCreateValidationFaild()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );
        IoC::register( 'AppleValidator', '\\TestStubs\\AppleRepo\\AppleValidatorFaildStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ExecutionErrorExceptions\ValidationFaildException'
        );
        $appleRepo->create( array ( ) );
    }

    public function testCreateValidationClassNotExist()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );
        unset( IoC::$registry['AppleValidator'] ); // IoC登録解除

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ProgramErrorExceptions\ValidatorClassNotFoundException'
        );
        $appleRepo->create( array ( ) );
    }

    public function testReadNormalTerminal()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $apple = $appleRepo->Read( 1 );

        $this->assertEquals( '姫リンゴ', $apple->name );
    }

    public function testReadNoID()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleNullReturnStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ExecutionErrorExceptions\IdNotFoundException'
        );

        $apple = $appleRepo->read( 1 );
    }

    public function testUpdateNormalTerminal()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );
        IoC::register( 'AppleValidator', '\\TestStubs\\AppleRepo\\AppleValidatorPassStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $appleRepo->update( 1, array ( ) );
    }

    public function testUpdataNoId()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleNullReturnStub' );
        IoC::register( 'AppleValidator', '\\TestStubs\\AppleRepo\\AppleValidatorPassStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ExecutionErrorExceptions\IdNotFoundException'
        );

        $appleRepo->update( 1, array ( ) );
    }

    public function testUpdateValidationFaild()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );
        IoC::register( 'AppleValidator', '\\TestStubs\\AppleRepo\\AppleValidatorFaildStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ExecutionErrorExceptions\ValidationFaildException'
        );

        $appleRepo->update( 1, array ( ) );
    }

    public function testUpdateValidationClassNotExist()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );
        unset( IoC::$registry['AppleValidator'] ); // IoC登録解除

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ProgramErrorExceptions\ValidatorClassNotFoundException'
        );

        $appleRepo->update( 1, array ( ) );
    }

    public function testDeleteNormalTerminal()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $appleRepo->delete( 1 );
    }

    public function testDeleteNoId()
    {
        IoC::register( 'Apple', '\\TestStubs\\AppleRepo\\AppleNullReturnStub' );

        $appleRepo = IoC::resolve( 'AppleRepo' );

        $this->setExpectedException(
            'ProjectH\Exceptions\ExecutionErrorExceptions\IdNotFoundException'
        );

        $appleRepo->delete( 1 );
    }

}

スタブ

最初に時間に関するフィールドを含んでいないApple Eloquentモデルのスタブです。

<?php

namespace TestStubs\AppleRepo;

class AppleEloquentStub extends \Eloquent
{

}

正直あまりスタブの意味はありません。時間に関するフィールドをORMに持たせる場合、そのORMはどの時点でフィールドの値を設置するか分かりません。(もちろん、コアを読めば分かりますが、基本コアはブラックボックス的に使用するものです。)

このスタブは「同じインスタンスが生成されている」ことを証明するためのスタブです。ですから、時間に関するフィールドを避けていることを保証するためにわざわざAppleと区別しました。ORMの中にはテーブルのフィールドまで自動に読み込んでくれるものがあります。(その分、遅くなります。)ですから、テーブルも存在しないことを証明するため、Appleクラスを避けました。(Elqoeuntはそこまで親切でありません。ですから、余り意味がありません。)

例え時間に関するフィールドを避けたとしても、ORMのクラスの内部でインスタンスの生成時間を持っている場合、時間が変わってしまい、インスタンスとして一致しないこともあります。(実際、今回のテストは当方の環境で全部で1秒かかりません。そのため、もっと遅いマシンで実行すると、テストに失敗するかも知れません。Eloquentの内部まで調べ尽くしているわけでありませんので。)

もし、時間に引っかかり直接の比較ができない場合は、関数でインスタンスのクラス名を比較するとか、工夫が必要になります。

次のスタブもApple Eloquentモデルのスタブですが、Eloquentを拡張してはいません。

<?php

namespace TestStubs\AppleRepo;

class AppleNullReturnStub
{

    function create( $data )
    {
        return null;
    }

    function find( $id )
    {
        return null;
    }

    function save()
    {

    }

    function delete ()
    {
        return 0;
    }

}

スタブらしいスタブです。 :D

IDが見つからないエラーの場合のためのもので、実際にIDが見つからないエラーはfind()メソッドの返すnullで判断されています。それ以外は今のところおまけです。

今回のプロジェクトのリポクラスは完全ではありません。例えばEloquentのupdate()やdelete()は影響を与えたレコード数を返します。今回のリポクラスでは1レコードだけに影響しますので、1が返ってくるのですが、それをチェックしていません。これが起きるのは、データベースが読み込み専用になっていたり、データベースユーザーに読み込み権限はあっても、書き込み権限が無い場合と思われます。こうした場合はI/Oエラーなどとして取り扱うべきでしょう。

次のスタブもAppleモデルのものですが、やはりEloquentを使用していません。

<?php

namespace TestStubs\AppleRepo;

class AppleStub
{
    public function __construct( $data = array ( ) )
    {

        if ( empty( $data ) )
        {
            $this->id = 1;
            $this->name = '姫リンゴ';
            $this->color = '赤緑';
        }
        else
        {
            $this->id = $data['id'];
            $this->name = $data['name'];
            $this->color = $data['color'];
        }
    }

    function create( $data )
    {
        return new static();
    }

    function find( $id )
    {
        return $this;
    }

    function fill( $data )
    {

    }

    function delete( )
    {

    }

    function save ()
    {

    }

}

Eloquentモデルの動きをシミュレートしつつも、同じ値を返します。Eloquentを拡張するよりは簡単です。簡単なEloquentのCRUD操作スタブであれば、これで十分でしょう。

残りの2つはバリデタークラスのスタブです。最初は正常にバリデーションを終了する、つまり例外を投げないスタブです。

<?php

namespace TestStubs\AppleRepo;

class AppleValidatorPassStub
{
    function validate_create( $data )
    {

    }

    function validate_update( $id, $data )
    {

    }
}

最後はバリデーション失敗を示す例外を投げるバリデターです。

<?php

namespace TestStubs\AppleRepo;

use ProjectH\Exceptions\ExecutionErrorExceptions\ValidationFaildException;
use Validator;

class AppleValidatorFaildStub
{

    function validate_create( $data )
    {
        $val = Validator::make( array ( ), array ( ) );
        throw new ValidationFaildException( $val );
    }

    function validate_update( $id, $data )
    {
        $val = Validator::make( array ( ), array ( ) );
        throw new ValidationFaildException( $val );
    }

}

注意点

テストコードに戻りましょう。

最初のテスト、testSetAppleClassPropertyInConstructorはコンストラクターの中でAppleクラスのインスタンスを獲得していることを確認するためのテストです。

プロパティーのappleはprivateですが、reflectionクラスを利用して読み取り、IoCコンテナで指定したクラスのインスタンスが保存されていることを確認しています。

バリデーションクラスがIoCコンテナに登録されていない状況を作成するため、testUpdateValidationClassNotExistメソッドでは、unset( IoC::$registry['AppleValidator'] );と実行しています。これはIoCクラスには一度登録したクラスをキャンセルするメソッドはないため、登録内容の配列へキーを制定して、アイテムをunsetしています。通常のコードではこんなことをする必要はありませんから、テストにだけ使用するコーディングです。

また、AppleValidatorはIoCにsingletonとして登録してありますが、IoCクラスでsingletonの再登録は上手く動作しないようです。(多分、一度インスタンスを生成してしまうと、再登録を受け付けない。)そのため、テスト時の置き換えはregisterメソッドで実行しています。

前回のバリデターのテストがテストパターンを多くテストするという意味でテストらしいテストでしたが、今回のリポジトリーは下部のクラスをスタブに置き換え実行するという意味で、単体テストらしい単体テストでした。