Using Model Factory States To Create Related Models
Fri, Jul 14, 2017Laravel’s model factories can be used to automatically create related models when the factory is called. This is a great way to DRY up the arrange portion of your tests. Consider the following factory definition:
$factory->define(Comment::class, function ($faker) {
return [
'post_id' => factory(Post::class)->lazy(),
'content' => $faker->sentence()
];
});
This factory will create a Comment along with a Post whenever it is called unless the Post is explicitly provided.
// creates a Comment and a Post
factory(Comment::class)->create();
// uses the provided Post instead of creating one
$post = factory(Post::class)->create();
$comment = factory(Comment::class)->create([
'post_id' => $post->id
]);
While the first example can save you from having to repeat the Post creation call across your test suite, I think it suffers from two problems. First, when a Comment is created with $comment = factory(Comment::class)->create();
I cannot tell just by looking at the test that a Post has also been created. This implicit behavior could confuse my future self or anyone else working with the code. Second, I will no longer be able to use the factory as an argument to the save() method:
$post->comments()->save(factory(Comment::class)->make());
This code will correctly assign the comment to the post, as expected. However, since the post_id is not explicitly provided to the comment factory an additional unassigned Post will be created.
Despite its convenience I think these two problems limit the usefulness of the lazy() method. Fortunately Laravel gives us a solution - model factory states:
$factory->define(Comment::class, function ($faker) {
return [
'content' => $faker->sentence()
];
});
$factory->state(Comment::class, 'withPost', function ($faker) {
return [
'post_id' => factory(Post::class)->lazy(),
];
});
By defining a state we can use the base Comment factory as an argument to the save() method without an additional Post being created. When we need a Comment with a Post we can be explicit and use the withPost
state.
// No unassigned post since the base Comment factory does not define it
$post->comments()->save(factory(Comment::class)->make());
// Creates the Post but is explicit about it
$comment = factory(Comment::class)->states(['withPost'])->create();