この記事について
「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 さん!