Laravel/データベースレイヤーとのテスト1

Posted: 2015-05-23 23:11 |  laravel PHP全般 
5.1が目前ですが、Laravelアプリケーションのテストやってますか?
便利な機能がたくさんあるなかでも、一番人気があるのはEloquentでしょうか?
Eloquentを用いたアプリケーションでのテストについていくつか紹介しましょう。
データベースなどを用いた値などのデータレイヤー(リポジトリ、エンティティなど)の実装を
シンプルに、ドキュメントなどにある例で実装してみましょう。
(本エントリではモデルという抽象的な表現は用いません。MVCアーキテクチャ一辺倒の内容ではないからです)

一般的な実装例

マイグレーションファイルとして下記のように作成します
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

/**
 * Class Entries
 */
class Entries extends Migration
{

    /** @var string  */
    protected $table = 'entries';

    /**
     * Run the migrations.
     * @return void
     */
    public function up()
    {
        Schema::create($this->table, function (Blueprint $table) {
            $table->increments('entry_id');
            $table->string('title')->index('entry_title');
            $table->longText('content');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     * @return void
     */
    public function down()
    {
        Schema::drop($this->table);
    }
}
データアクセスレイヤとしてApp\Repositories\Eloquent\EntryEntityとして
Eloquentを継承したクラスを作成します。
namespace App\Repositories\Eloquent;

use Illuminate\Database\Eloquent\Model as Eloquent;

/**
 * Class EntryEntity
 * @package App\Repositories\Eloquent
 */
class EntryEntity extends Eloquent
{

    /** @var string  */
    protected $primaryKey = 'entry_id';

    /** @var string  */
    protected $table = 'entries';

    /** @var array  */
    protected $fillable = [
        'title',
        'content'
    ];

}
ここでは通常のEloquentORMとして利用します。
極端な例ですが、一般的なサンプルではコントローラで
以下のようにして使うのが多いので同じようにしましょう。
namespace App\Http\Controllers;

use App\Repositories\Eloquent\EntryEntity;

/**
 * Class EntryController
 * @package App\Http\Controllers
 */
class EntryController extends AbstractController
{

    public function index()
    {
        $entries = EntryEntity::all()->toArray();
        if(!count($entries)) {
            throw new \Exception;
        }
    }
}
よく見る例と同じですね。
ではこれの問題点はどこにあるでしょうか?
一般的によく見られるように、ここではコントローラですが
このクラスはEntryEntityが動作しなければそもそも動かず、EntryEntityはデータベースがなければ動作しません。
(toArray()にしているのは後ほど)

Eloquentによるテスト

テストは一般的にデータベースを利用せずに行います。
データベースでエラーが起きる場合や、書き込みなどが走る場合もありますし、
エラー原因がアプリケーションで起きているものなのか、外部ミドルウェアで起きているものなのかを
切り分ける必要もあります。
テストを行うのはあくまでphpアプリケーションに対して行います。
コントローラのテストは下記の例のようにHTTPアクセスで実行する場合は、
コントローラ単体のテストではなく、ルータやミドルウェアなどすべての要素をテストするため、
データベースは除外するべきでしょう!
(以下の例は単体で動かさないため、ユニットテストではなくアプリケーション、ファンクショナルテストです)

このコントローラをデータベースを利用せずにテストするには
mockeryを利用してEntryEntityをモックします。
use Mockery as m;

class EntryControllerTest extends TestCase
{

    public function tearDown()
    {
        parent::tearDown();
        m::close();
    }

    public function testIndex()
    {
        $eloquentMock = m::mock('overload:App\Repositories\Eloquent\EntryEntity');
        $collection = m::mock('Illuminate\Database\Eloquent\Collection');
        $eloquentMock->shouldReceive('all')->andReturn($collection);
        $collection->shouldReceive('toArray')->andReturn([1, 2, 3]);

        $this->call('GET', '/entry');
        $this->assertResponseOk();
        // Laravel5.1ではこう記述することもできます
        $this->visit('/entry'); 
    }
}
mokeryでEntryEntityクラスをモックし、collectionやtoArrayで返却されるものを変更します。
同時にモッククラスであるため、データベースも利用しません。
この例ではEntryEntityクラスのみを利用しているため、モックは一つでしたが、
実践的なアプリケーションではもっとたくさんのデータベースアクセスレイヤの処理が必要となります。
たくさんのEloquentを継承したクラスがある場合、どうするのがベストでしょうか?

処理の分離

一番シンプルなリファクタリングとしての一歩は、
よく言われるように利用しているクラスを外で指定するようにします。
コントローラのファンクショナルテストを行う場合、
メソッドインジェクションやコンストラクタインジェクションを利用するため、
利点がないように見えますが、下記の例を見てみましょう。
class EntryController extends AbstractController
{

    /**
     * @param EntryEntity $entry
     * @throws \Exception
     */
    public function index(EntryEntity $entry)
    {
        $entries = $entry->all()->toArray();
        if(!count($entries)) {
            throw new \Exception;
        }
    }
}
メソッドインジェクションを利用すると上記のようになります。
テストコードは大きく変わりません。
さらにこのコードをテストしやすく、モックを利用しないように変更します。
モックは一見簡単ですし、慣れてしまえば確かに楽ですが、
mockeryの利用方法がわからなければ暫くは辛いと思います。
一般にテストがしやすい、と呼ばれるものは疎結合、
この例で言うとコントローラがデータベースを利用するクラスに依存していねければ済むわけです。
EntryEntityを疎結合にするにはインターフェースを利用します。
難しいと思っても大丈夫!じっくりやりましょう。
下記のようなインターフェースを用意します。
namespace App\Repositories;

/**
 * Interface EntryRepository
 * @package App\Repositories
 */
interface EntryRepositoryInterface
{
    
}
インターフェースを実装します。
namespace App\Repositories\Eloquent;

use App\Repositories\EntryRepositoryInterface;
use Illuminate\Database\Eloquent\Model as Eloquent;

/**
 * Class EntryEntity
 *
 * @package App\Repositories\Eloquent
 */
class EntryEntity extends Eloquent implements EntryRepositoryInterface
{

    /** @var string  */
    protected $primaryKey = 'entry_id';

    /** @var string  */
    protected $table = 'entries';

    /** @var array  */
    protected $fillable = [
        'title',
        'content'
    ];

}
コントローラはこのようになります。
    /**
     * @param EntryRepositoryInterface $entry
     * @throws \Exception
     */
    public function index(EntryRepositoryInterface $entry)
    {
        $entries = $entry->all()->toArray();
        if(!count($entries)) {
            throw new \Exception;
        }
    }
タイプヒンティングがインターフェースに変わりました。
EntryEntityからインターフェースに変わったことで、ここで利用するクラスは
EntryEntityクラスではなく、このインターフェースが実装されたクラスであればなんでもよくなります。
ただ、まだ一つ問題があります。
Eloquentのメソッドであるallなどが残っています。
テストをしやすくするためにインターフェースに変更し、EntryEntityにそのインターフェースを実装したので、
たしかにEntryEntityを利用する場合は問題ありません。
が、テストをするのにEntryEntityと同じようにインターフェースを実装した、
データベースを利用しないようにしたクラスを用意して・・

となるとテストがしやすいどころか面倒なだけです。
ではインターフェースにメソッドを追加して少し工夫しましょう。
interface EntryRepositoryInterface
{

    /**
     * @return array
     */
    public function getAll();
}
getAllというメソッドを用意して、allとtoArrayを利用して配列を返すメソッドとして実装します。
allでCollectionを返却するため利用するのは楽ですが、テストがしやすいわけではありません。
EntryEntityクラスにgetAllメソッドを実装して下記のようにします。
    /**
     * @return array
     */
    public function getAll()
    {
        return $this->all()->toArray();
    }
EntryEntityクラスを利用してリポジトリパターンを実装しても良いですが、
ここでは簡単にいきます。
サービスプロバイダのregisterメソッドでタイプヒンティングしたインターフェースと、
実際に利用するクラス(具象クラス)をバインドします
       $this->app->bind(
            'App\Repositories\EntryRepositoryInterface',
            'App\Repositories\Eloquent\EntryEntity'
        );
コントローラは下記のようにメソッドを変更します。
class EntryController extends AbstractController
{

    /**
     * @param EntryRepositoryInterface $entry
     * @throws \Exception
     */
    public function index(EntryRepositoryInterface $entry)
    {
        $entries = $entry->getAll();
        if(!count($entries)) {
            throw new \Exception;
        }
    }
}
これでEloquentを利用したEntryEntityクラスにまったく依存しなくなりました。
いつも通りHTTPリクエストでアクセスしても最初とまったく同じ動作をします。
ではテストコードに戻りましょう。
インターフェースに依存しているのがわかりましたので、
同じようにインターフェースを実装したダミーのクラス(スタブ)を作り、テスト時に動作を変更します。
 
class EntryControllerTest extends TestCase
{

    public function testIndex()
    {
        $this->app->bind(
            'App\Repositories\EntryRepositoryInterface',
            'EntryStubRepository'
        );
        $this->call('GET', '/entry');
        $this->assertResponseOk();
    }
}

class EntryStubRepository implements \App\Repositories\EntryRepositoryInterface
{

    /**
     * @return mixed
     */
    public function getAll()
    {
        return [
            1, 2, 3
        ];
    }
}
これでモックではなく、スタブに差し替えてデータベースを利用せずにコントローラのテストができるようになりました。
インターフェースを利用したことにより、テストではなくても
実際に使用変更などが発生した場合にも簡単に対応できるようになります。
では、データベースを利用したクラスはどうやってテストするのでしょうか?
次はそのヒントを紹介しましょう!
 

about ytake

執筆に参加しています


Laravel お役立ち情報

share



このエントリーをはてなブックマークに追加

Categories

laravel 45

DTM 0

music 0

PHP全般 31

0

JAPAN 1

WORLD 1

javascript 4

RDBMS 1

NoSQL 1

NewSQL 1

Recent Posts

Ad

comments powered by Disqus

GitHub

Social Links

Author


クリエイティブ・コモンズ・ライセンス
Yuuki Takezawa 作『Ytake Blog』はクリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンス で提供されています。

© ytake/comnect All Rights Reserved. 2014