s平面の左側

左側なので安定してます(制御工学の話は出てきません)

【Laravel】Repository パターンを使った際に DB アクセスを発生させないテストを書く

この記事について

Laravel Advent Calendar 2018」19日目の記事。

前提

レイヤードアーキテクチャを適用している次のようなディレクトリ構成を想定する。

説明したいことにフォーカスするため Value Object などは省略している。

app/
|-- Providers/
|  `-- RepositoryServiceProvider.php
|-- Domain/
|  |-- User.php
|  `-- UserRepository.php
`-- Infrastructure/
   `-- Eloquents/
     `- User.php

Domain レイヤーには User の Entity が定義されている。

<?php

namespace App\Domain;

class User
{
    protected $id;
    protected $userName;

    public function __construct(int $id, string $userName)
    {
        $this->id = $id;
        $this->userName = $userName;
    }

    // 略(ドメイン知識を表現するメソッド郡)
    // :
}

また、User に対する Repository の interface も定義されている。

<?php

namespace App\Domain;

interface UserRepository
{
    public function findById(int $id): ?User;
}

Infrastructure レイヤーには users テーブルの Eloquent ORM があり、UserRepository interface の実装を満たしている。

<?php

namespace App\Infrastructure\Eloquents;

use Illuminate\Database\Eloquent\Model;
use App\Domain\User as UserEntity;
use App\Domain\UserRepository;

class User extends Model implements UserRepository
{
    // :
    // 略($fillable, $casts などの設定)

    public function toEntity(): UserEntity
    {
        return new UserEntity(
            $this->id,
            $this->user_name
        );
    }

    public function findById(int $id): ?UserEntity
    {
        $user = $this->find($id);

        return is_null($user) ? null : $user->toEntity();
    }
}

Eloquent ORM から Entity に変換する toEntity() メソッドを定義し、公開しておくのがポイント。

アプリケーションを動作させる際には ServiceProvider にて interface と実装を結びつける。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\UserRepository;
use App\Infrastructure\Eloquents\User;

class RepositoryServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->app->bind(UserRepository::class, User::class);
    }
}

DB アクセスを発生させないテストの書き方

あらかじめ model factory を書いておく。

<?php

use Faker\Generator as Faker;

$factory->define(App\Infrastructure\Eloquents\User::class, function (Faker $faker) {
    return [
        'user_name' => $faker->userName,
    ];
});

これにより factory()->make(App\Infrastructure\Eloquents\User::class) とすれば、DB アクセスを発生させずに、ダミーデータで App\Infrastructure\Eloquents\User のインスタンスを生成できる。

これを活用してテストコードを書くと下記のようになる。

Mock の作成には Mockery を使っている。

<?php

namespace Tests\Unit;

use App\Domain\UserRepository;
use App\Infrastructure\Eloquents\User as UserEloquent;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * @test
     */
    function 何かしらのテスト()
    {
        // Repository が返す Entity をダミーデータで生成
        $userEntity = factory(UserEloquent::class)
            ->make()
            ->toEntity()

        // UserRepository の Mock 作成
        $repositoryMock = \Mockery::mock(UserRepository::class);
        $repositoryMock->shouldReceive('findById')
            ->andReturn($userEntity);

        // UserRepository interface と Mock を結びつける
        $this->app->instance(UserRepository::class, $repositoryMock);

        // テストを書く
        // ロジックの内で呼び出される UserRepository::findById() は上記 Mock のに置き換えられる
        // :
    }
}

これによって、 DB アクセスを発生させないテストを書くことができる。

ダミーデータの特定のカラムに任意の値をセットしたければ下記のように make() メソッドに連想配列の引数を渡せばよい。

<?php

use App\Infrastructure\Eloquents\User as UserEloquent;

$user = factory(UserEloquent::class)->make([
    'user_name' => 'jhon_doe',
])

echo $user->user_name; // 'jhon_doe'

model factory を使った書き方だと users テーブルの定義に変更があった場合も model factory を修正するだけでテストコード側を修正する必要がなくなるので嬉しい。

明日の Laravel Advent Calendar 2018 の担当は

yamotuki さん!