こんにちはみなさん
@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();
「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();
factory(User::class)->state('管理者')->create();
factory(User::class)->states('管理者', 'Bob')->create();
factory(User::class)->states('Bob', '管理者')->create();
コメントアウトしたものがログに出力されたものになります。
afterCreatingで設定された処理は必ずデータ生成後にはじめに呼び出され、その後、stateごとのコールバックが呼び出されます。
複数のstateを設定している場合はstates
の引数に指定した順にコールバックが呼び出されます。
defaultの扱い
stateにおいて、「default」という名前は特殊な扱いを受けます。
おもむろに
<?php
$factory->afterCreatingState(App\User::class, 'default', function ($user) {
\Log::default('default');
});
と書いて、先の実施順の処理を実行すると
<?php
factory(User::class)->create();
factory(User::class)->state('管理者')->create();
factory(User::class)->states('管理者', 'Bob')->create();
このように、どんなときでも「default」で指定した処理が流れます。
実装上はafterCreating
の内部ではafterCreatingState($class, 'default', $callback)
が呼ばれているので、さもありなんです。
defaultはもともと一番はじめのstateのない状態なので、明示的に定義する必要はないと思いますので、stateに定義したり、使用したりしないほうがいいでしょう。
まとめ
というわけで、LaravelのFactoryのstateの細かい話をしました。
LaravelのFeatureテストを書いていると、テストデータ作成がどんどん膨らんでいくので、そのあたりをstateを使って効率化したいところです。
また、マニュアルにはないものの、複数の状態をもたせることもできるので、stateの定義数も最適化していけるといいと思います。
最後に
弊社は急速に成長しているプロダクトを複数抱えており、この成長を支えてくれるデザイナーやエンジニアを募集しています。
www.wantedly.com
www.wantedly.com