Laravelのテストで使うFactoryのstateのちょっと細かい話

こんにちはみなさん
@niisan-tokyoです。

Laravelを使うなら、当然テストも一緒に書くわけですが、テストの条件とかがどんどん複雑になってくると、可読性も落ちていろいろとやりにくくなります。
Laravelのテストデータを作成するFactoryには、複雑さを緩和するためにstateという機能があって、各条件・状態ごとに名前をつけることができ、そのstateごとに値を変えたり、後処理を入れたりすることができます。 ということで、今回はそのFactoryに実装されるstateをどのように使っていくか見てみましょう。

Factoryのstate

早速Factoryのstateを使ってみましょう。
まず、普通にUserのFactoryを作ると以下のような感じになります。

<?php
use Faker\Generator as Faker;

$factory->define(App\User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => bcrypt('secret')
        'is_admin'   => false
    ];
});

こうして作成されたユーザーはis_adminfalseになります。ここでis_adminが管理者フラグを表すとすれば、Factoryで作られたユーザーは管理者ではありません。 管理者ユーザーをFactoryを通して作成するためには、テストの中で以下のように書きます。

<?php
//...
$user = factory(User::class)->create(['is_admin' => true]);

データを作るたびに['is_admin' => true]を書くのはちょっと面倒くさいです。
そこで、「ユーザーが管理者である」という状態をFactoryに追加します。

<?php
//...
$factory->state(App\User::class, '管理者', function () {
    return [
        'is_admin' => true
    ];
});

これを追加しておくことで、テストでは

<?php
//...
$user = factory(User::class)->state('管理者')->create();
$this->assertTrue($user->is_admin);

のように書けます。

複数stateの持ち方

公式マニュアルにないので使って良いものかとは思うのですが、複数のstateを指定する方法もあります。 まず、

<?php
//...
$factory->state(App\User::class, 'Bob', function () {
    return [
        'name' => 'Mr. Bob'
    ];
});

こんな感じのstateを定義したあと、

<?php
//...
    public function testMultiState()
    {
        $user = factory(User::class)->states('管理者', 'Bob')->create();
        $this->assertTrue($user->is_admin);
        $this->assertEquals('Mr. Bob', $user->name);
    }

のように書くことで、複数のstateを同時に指定できます。

なお、複数stateでのmakeやcreateの実装を見ると、stateによる生成データの上書きは後勝ちになるため、

<?php
//...
$factory->state(App\User::class, 'Bob', function () {
    return [
        'name' => 'Mr. Bob',
        'is_admin' => false
    ];
});

のように、重複要素が指定されると、statesの引数の順番によってデータの状態が変わります。

<?php
//...

    /**
     * @test
     */
    public function 後勝ちなのでadminになるケース()
    {
        $user = factory(User::class)->states('Bob', '管理者')->create();
        $this->assertTrue($user->is_admin);
    }

    /**
     * @test
     */
    public function 後勝ちなのでadminにならないケース()
    {
        $user = factory(User::class)->states('管理者', 'Bob')->create();
        $this->assertFalse($user->is_admin);
    }

statesにたくさんの引数をもたせると、問題になるケースはありそうですので、最小限のstateにしぼりましょう。

afterCreating**

テストデータ生成後に追加で他のデータを生成したり、何らかのコールバック処理をしたい場合はafterCreatingを使ってコールバックを定義できます。
(なお、afterMakingでも同じような議論ができますが、割愛します。)

コールバック処理の定義は以下のようにFactoryに書きます。

<?php
//...
$factory->afterCreating(App\User::class, function ($user) {
    factory(App\Post::class)->create(['user_id' => $user->id]);
});

一方で、特定の条件下でのみコールバック処理したい場合はstateを利用することができます。

<?php
//...
$factory->afterCreatingState(App\User::class, 'Bob', function ($user) {
    echo $user->name;
});

このように定義することで

<?php
//...
$user = factory(User::class)->state('Bob')->create();
// Mr. Bob

「Bob」というstateを指定したときだけ、標準出力にMr. Bobというユーザー名が出力されるようになります。

afterCreatingの実施順

基本的に、実施する順番に依存するのはよくないのですが、そうも言ってられない場合があります。
実際にどのようにコールバック処理が実施されるかを検証してみましょう。

<?php
//...
$factory->afterCreating(App\User::class, function ($user) {
    \Log::debug('no state');
});

$factory->afterCreatingState(App\User::class, '管理者', function ($user) {
    \Log::debug('管理者');
});

$factory->afterCreatingState(App\User::class, 'Bob', function ($user) {
    \Log::debug('Bob');
});

のようにコールバック処理を定義したとき、以下の処理を実行してみます。

<?php
//...
        factory(User::class)->create();
        //  no state

        factory(User::class)->state('管理者')->create();
        //  no state
        //  管理者

        factory(User::class)->states('管理者', 'Bob')->create();
        //  no state
        //  管理者
        //  Bob

        factory(User::class)->states('Bob', '管理者')->create();
        //  no state
        //  Bob
        //  管理者

コメントアウトしたものがログに出力されたものになります。
afterCreatingで設定された処理は必ずデータ生成後にはじめに呼び出され、その後、stateごとのコールバックが呼び出されます。
複数のstateを設定している場合はstatesの引数に指定した順にコールバックが呼び出されます。

defaultの扱い

stateにおいて、「default」という名前は特殊な扱いを受けます。
おもむろに

<?php
//...
$factory->afterCreatingState(App\User::class, 'default', function ($user) {
    \Log::default('default');
});

と書いて、先の実施順の処理を実行すると

<?php
//...
        factory(User::class)->create();
        //  no state  
        //  default  

        factory(User::class)->state('管理者')->create();
        //  no state  
        //  default  
        //  管理者  

        factory(User::class)->states('管理者', 'Bob')->create();
        //  no state  
        //  default  
        //  管理者  
        //  Bob  

このように、どんなときでも「default」で指定した処理が流れます。
実装上はafterCreatingの内部ではafterCreatingState($class, 'default', $callback)が呼ばれているので、さもありなんです。

defaultはもともと一番はじめのstateのない状態なので、明示的に定義する必要はないと思いますので、stateに定義したり、使用したりしないほうがいいでしょう。

まとめ

というわけで、LaravelのFactoryのstateの細かい話をしました。
LaravelのFeatureテストを書いていると、テストデータ作成がどんどん膨らんでいくので、そのあたりをstateを使って効率化したいところです。
また、マニュアルにはないものの、複数の状態をもたせることもできるので、stateの定義数も最適化していけるといいと思います。

最後に

弊社は急速に成長しているプロダクトを複数抱えており、この成長を支えてくれるデザイナーやエンジニアを募集しています。

www.wantedly.com

www.wantedly.com