こんにちはみなさん
@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_admin
がfalse
になります。ここで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の定義数も最適化していけるといいと思います。
最後に
弊社は急速に成長しているプロダクトを複数抱えており、この成長を支えてくれるデザイナーやエンジニアを募集しています。