Make the most out of Laravel Factories: How we migrated from Factory Muffin

We use Laravel to power the Knack API that is consumed by our iOS, Android, and Web applications, which are built in React using TypeScript.

When writing tests for our Laravel backend we rely on factory methods to generate the objects being used in our tests. Prior to this year all of our factory methods were created using Factory Muffin, whose stated goal is to “enable the rapid creation of objects for the purpose of testing.” However, Laravel also offers factory methods, which we decided to migrate to, first slowly, and then we picked up our pace quite quickly. We make it a point to recognize where we can use native features of Laravel and therefore lean less heavily on a 3rd party library, so migrating to Laravel factories was a natural choice.

When this project began we had 52 factory model definitions in factories.php, which Factory Muffin uses to define objects that are instantiated by our test suite. It feels good to reflect on our progress now that we have completely removed all of our Factory Muffin factories and the library itself from our composer.json.

Factory Muffin Example

$fm->define(Organization::class)->setDefinitions([
   'name' => Faker::company(),
   'type' => 'school',
   'active' => true,
]);

There were a few factors that dictated how difficult a given model was to migrate. If an object has fewer relations to other objects then it required less work when migrating to Laravel Factories, so we churned through the majority easily. The last few were pretty meaty and had references to all sorts of objects that had references to even more objects. Some had callbacks that would set properties after object creation and we customized others to use Laravel’s factory states.

Going from Factory Muffin to Laravel factories was a rather smooth transition. They both generate factories in the same way, as far as the engineer using them in tests is concerned. We have a custom DataFactory module for our Codeception tests which provides helper methods for rapidly generating models in tests using the $I->haveFoo() pattern. For our migration all we needed to do was update our custom DataFactory class so we didn’t have to make sweeping modifications to our existing test suites.

Laravel Factory Example

class OrganizationFactory extends Factory
{
    protected $model = Organization::class;

    public function definition(): array
    {
        return [
            'name' => $this->faker->company,
            'type' => 'school',
            'active' => true,
        ];
    }
}

Codeception DataFactory examples:

public function haveOrganization(array $attrs = []): Organization {
        //using Laravel Factories
        return Organization::factory()->create($attrs);
    }
public function haveOrganization(array $attrs = []): Organization {
        //using Factory Muffin
        return $this->have(Organization::class, $attrs);
    }

In this example we are defining our factory method that will be used when $I->haveOrganization() is called in one of our tests. Of all the attributes available to an Organization object, the above code dictates that we always want to define a random name, make the type be “school”, and always have the organization object active. These attributes can be overridden by passing an array of attributes and desired values to $I->haveOrganization() like so:

$I->haveOrganization([
    ‘name’ => ‘Knack University’,
    ‘active’ => false,
    ‘slug’ => ‘knack-university’
])

This would result in instantiating an organization object named Knack University, that is a school, inactive, and has the slug “knack-university”. Here we override name and type, and although a slug is not automatically generated for Organizations we decided that for this particular organization object we want a specific slug of “knack-university”.

When using either Factory Muffin or Laravel’s native factories the format $I->haveOrganization() is used and an array of custom attributes can be passed to instantiate objects. Because of this, no update needs to be made to our tests themselves. The main reason we made this migration from Factory Muffin to Laravel factories has been to utilize a native feature of Laravel that we were not previously utilizing. Removing a dependency and easing up slightly on the overhead that goes with dependency management is always nice as well!