Laravel Logo

Now that we know how to create tests, it’s time we looked at how we can use factories to generate test data quickly and easily.

Why We Should Use Seeders

Let’s start with an example to work through why we need seeders. We’re going to create a new piece of logic for our Project class and say that if a Project has a null end_date column then it hasn’t been completed. To test this we’re going to create a simple unit test (in Tests\Unit\ProjectTest) that looks like the following:

public function testNullEndDateIndicatesNotCompeted()
{
    $item = new Project();
    $item->name = 'Test Name';
    $item->start_date = now();
    $item->end_date = null;

    $this->assertTrue($item->isNotComplete(), 'null end_date => not completed');
}

If we run this we’ll get an error indicating that the function doesn’t exist. When we create the function we could just stick in a return false and call it good for this phase of the TDD cycle but the implementation for this function is so trivial we’re just going to write it directly:

public function isNotComplete(): bool
{
    return $this->end_date == null;
}

When we run the tests again we’ll be back to a green bar. Now to check the opposite.

public function testNullEndDateIndicatesNotCompeted()
{
    $item = new Project();
    $item->name = 'Test Name';
    $item->start_date = now();
    $item->end_date = null;

    $this->assertTrue($item->isNotComplete(), 'null end_date => not completed');
}

public function testNonNullEndDateIndicatesCompeted()
{
    $item = new Project();
    $item->name = 'Test Name';
    $item->start_date = now();
    $item->end_date = now();

    $this->assertFalse($item->isNotComplete(), 'not null end_date => completed');
}

Great! But now we have a lot of duplication between the two tests. The first three lines are identical and the forth is almost identical so we can extract a function and reduce some duplication:

public function testNullEndDateIndicatesNotCompeted()
{
    $item = $this->createNotCompletedProject();
    $this->assertTrue($item->isNotComplete(), 'null end_date => not completed');
}

public function testNonNullEndDateIndicatesCompeted()
{
    $item = $this->createNotCompletedProject();
    $item->end_date = now();
    $this->assertFalse($item->isNotComplete(), 'not null end_date => completed');
}

private function createNotCompletedProject(): Project
{
    $item = new Project();
    $item->name = 'Test Name';
    $item->start_date = now();
    $item->end_date = null;

    return $item;
}

We can be happy with this solution and check it into our SCM but now if we need to initialize a Project in another test we’ll have to do the same basic steps. We could add the function to Tests\TestCase but Laravel provides a better solution.

Enter Factories

We going to use artisan’s make:factory command to generate our factory for us.

The general format for this command is:

php artisan make:factory --model=<ModelName> <ModelName>Factory

For our example, we’re going to specify “Project” as the “ModelName”.

php artisan make:factory --model=Project ProjectFactory

Now we can look in “database/factories/ProjectFactory.php” and see the following code:

<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Project;
use Faker\Generator as Faker;

$factory->define(Project::class, function (Faker $faker) {
    return [
        //
    ];
});

To get us started we’re going to pull in the code we created in the createNotCompletedProject() function above.

$factory->define(Project::class, function (Faker $faker) {
    return [
        'name' => 'Some Name',
        'start_date' => now(),
        'end_date' => null,
    ];
});

Now that we’ve created our factory we can revisit Tests\Unit\ProjectTest and see how the factory affects what we’ve come up with. We’ll replace the lines containing $item = $this->createNotCompletedProject(); with $item = factory(Project::class)->make();. Then we can delete createNotCompletedProject().

public function testNullEndDateIndicatesNotCompeted()
{
    $item = factory(Project::class)->make();
    $this->assertTrue($item->isNotComplete(), 'null end_date => not completed');
}

public function testNonNullEndDateIndicatesCompeted()
{
    $item = factory(Project::class)->make();
    $item->end_date = now();
    $this->assertFalse($item->isNotComplete(), 'not null end_date => completed');
}

That looks a lot better.

Using Faker

When we setup our factory we set the Project’s name to “Some Name” which is quick way to get it setup but all of our Projects will have the same name.

public function testNullEndDateIndicatesNotCompeted()
{
    $item = factory(Project::class)->make();
    dd($item->name);
    $this->assertTrue($item->isNotComplete(), 'null end_date => not completed');
}
"Some Name"

Luckily, Laravel comes with the Faker library installed and setup. By default it’s passed as a parameter to our factories (note the Faker below).

$factory->define(Project::class, function (Faker $faker) {

The Faker library allows you to easily create test data that isn’t all identical. Its GitHub page (https://github.com/fzaninotto/Faker) has a complete list of all the values you can use but some of interest include:

$faker->email                   // 'tkshlerin@collins.com'
$faker->safeEmail               // 'king.alford@example.org'
$faker->sentence($nbWords = 6, $variableNbWords = true)  // 'Sit vitae voluptas sint non voluptates.'
$faker->name($gender = null|'male'|'female')      // 'Dr. Zane Stroman'
$faker->firstName($gender = null|'male'|'female') // 'Maynard'
$faker->firstNameMale                             // 'Maynard'
$faker->firstNameFemale                           // 'Rachel'
$faker->lastName                                  // 'Zulauf'

In our example we’re going to have it pick the name of a company for us to use.

$factory->define(Project::class, function (Faker $faker) {
    return [
        'name' => $faker->company,
        'start_date' => now(),
        'end_date' => null,
    ];
});

Now if we run our dd() test from above we get a company name that will change on every test.

"Feest Ltd"

Building Multiple Projects at once

There’s an optional parameter to the factory() helper function that tells it to return an array of the model instead of a single instance. This is helpful if you need to act on several copies of a model.

public function testWeCanGetMultipleProjects()
{
    $items = factory(Project::class, 5)->make();
    $this->assertSame(5, count($items));
}

States

Let’s say we need to initialize several completed Projects quickly. We can use that optional parameter we discussed above to generate them and then loop through them to in order to set their end_date.

public function testWeCanGetMultipleCompletedProjects()
{
    $items = factory(Project::class, 5)->make();
    $this->assertSame(5, count($items));

    foreach ($items as $item) {
        $item->end_date = now();
    }

    foreach ($items as $item) {
        $this->assertFalse($item->isNotComplete(), 'not null end_date => completed');
    }
}

This is a little cumbersome so thankfully Laravel has a better solution for us. Factory states allow us to define another set of changes that should be applied to the object create using the factory by adding values on top of the “defaults” set by the base factory.

If we open up our factory file for the Project class we can add a state function that explains how to create a “completed” Project.

// existing
$factory->define(Project::class, function (Faker $faker) {
    return [
        'name' => $faker->company,
        'start_date' => now(),
        'end_date' => null,
    ];
});

// new
$factory->state(Project::class, 'completed', function ($faker) {
    return [
        'end_date' => now(),
    ];
});

Now we can revisit our tests that explicitly set the end_date and apply our completed state to them.

public function testNonNullEndDateIndicatesCompeted()
{
    $item = factory(Project::class)->states('completed')->make();
    $this->assertFalse($item->isNotComplete(), 'not null end_date => completed');
}

public function testWeCanGetMultipleCompletedProjects()
{
    $items = factory(Project::class, 5)->states('completed')->make();
    foreach ($items as $item) {
        $this->assertFalse($item->isNotComplete(), 'not null end_date => completed');
    }
}

In this example it doesn’t clean up a lot of code but if we had to set two or more properties it would really help.

We can also setup Projects that have multiple states at once. For example, we can create a “startedLastYear” state that sets startDate to the current date minus a year and apply “startedLastYear” and “completed” in one function call.

$item = factory(Project::class)->states('completed', 'startedLastYear')->make();

Overriding the Defaults

Sometimes it’s necessary to override one of the values inside the factory. This can be done by passing an associative array of the items you want changed.

public function testWeCanOverrideName()
{
    $item = factory(Project::class)->make([
        'name' => 'This Programming Thing',
    ]);
    $this->assertEquals("This Programming Thing", $item->name);
}

Using Factories in Other Factories

Let’s say you have something like our Task class which requires a parent Project and the User who created it. If we just create a factory that looks like the following. Then it won’t work.

$factory->define(Task::class, function (Faker $faker) {
    return [
        'name' => 'task name',
    ];
});

We’ll get an error like this.

PDOException: SQLSTATE[HY000]: General error: 1364 Field ‘user_id’ doesn’t have a default value

To fix this problem we can use the factories defined for the User and Project classes to automatically create the required objects.

$factory->define(Task::class, function (Faker $faker) {
    return [
        'name' => 'task name',
        'user_id' => factory(\App\User::class),
        'project_id' => factory(\App\Project::class),
    ];
});

Conclusion

In conclusion, factories are a powerful tool that allows you to quickly create objects to perform tests on. They reduce duplicate code and make it easier to setup your tests.

Hopefully this has been as helpful to you as it has to me and check back soon for more artisan commands.