Using Model Factory States To Create Related Models Jul 14

Update 2019: my opinions on this topic have changed quite a bit since it was originally published. See the update section at the bottom of this article

Laravel’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();

Update 2019

I now disagree with much of what is written above. A model factory should include everything that is required to create a valid model (but nothing more). If a post is required to create a comment then that definitely belongs in the comment factory. Otherwise every time we use that factory in our tests we are going to have to deal with assigning the post. The original article listed two problems:

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 is a good thing. If the post is not needed by the test then I don't want to see it. Having details about the post in a test that is only testing the comment is noise that obscures the meaning of the test.

I will no longer be able to use the factory as an argument to the save() method … $post->comments()->save(factory(Comment::class)->make()); … since the post_id is not explicitly provided to the comment factory an additional unassigned post will be created.

I do like the simplicity of passing the factory to the save method but I no longer believe it's worth the tradeoffs. The alternative is not significantly worse:

$post = factory(Post::class)->create();
$comment = factory(Comment::class)->create([
    'post_id' => $post->id
]);

And it's fine because we are only going to do this when the test actually needs both the post and a comment. Our test setup code is inline with what is to come in the test.

I still use model factory states frequently, but when it comes to creating relationships with them I recommend them in situations where the related model is not a required field. A potential use case could be the other side of this comment -> post relationship:

$post = factory(Post::class)->states(['withComments'])->create();

This communicates that our test needs a post with some comments but the details of these comments are not going to be important.

Persisting Polymorphism May 1

Recently I worked on a project where a user's choice needed to permanently modify the behavior of an object. The gist of the project is that a user is creating a job posting and at some point they will choose how they wish to publish the job: using a job credit, as a daily rate, on a recruiting plan, etc. The choice of publishable type alters the behavior of the job every time the user decides to change its status (from draft to open, draft to scheduled, open to closed). For example: when a job credit job is changed from draft status to open status the system has to verify that a credit is available and then use the credit on the job, but a daily rate job would instead charge the users credit card.

You can imagine how this could lead to some pretty nasty code:

public function open()
{
    if ($this->publishable_type === 'credit') {
        // verify credit is available and anything else that
        // needs to be done before a credit job is opened
    } elseif ($this->publishable_type === 'daily') {
        // charge credit card or anything that needs to be
        // done before a daily job is opened
    } elseif ($this->publishable_type === 'plan' {
        // an elseif for any other types
    }

    $this->status = 'open';
    $this->save();
    // and any other code that has to be executed when a job is
    // opened regardless of which publishable type is chosen

    if ($this->publishable_type === 'credit') {
        // assign credit to job
    } elseif ($this->publishable_type === 'daily') {
        // whatever needs to be done for daily job
    } elseif ($this->publishable_type === 'plan' {
        // an elseif for any other types
    }
}

This implementation would result in the open() method being modified anytime a new publishable type is added. Similar methods would have to exist for the other job status changes as well. Not a maintainable approach. If we could abstract the behavior for each of the types into their own classes this method could be significantly improved. Consider if the open method could look like this instead:

public function open()
{
    $this->publishableType()->beforeOpen();

    $this->status = 'open';
    $this->save();

    $this->publishableType()->afterOpen();
}

Much nicer.

To make this work the publishableType() method needs to return a new instance of the type class. We can take a cue from Laravel’s polymorphic relationships and save the namespace of the type class to a publishable_type column on the job table. Meaning that the column would contain something along the lines of: App\Daily, App\Credit or App\Whatever. The publishableType() implementation looks like:

public function publishableType()
{
    return new $this->publishable_type($this);
}

And that is it. As long as each publishable type implements the same interface then changing the behavior of a job is a simple as persisting a different publishable type to the database.