loading...

Simplifying Laravel tests by using factory setup classes

mattkingshott profile image Matt Kingshott 👨🏻‍💻 Originally published at itnext.io on ・5 min read

This article is part of a series where I document insights, changes and rethinking that I experienced while refactoring the codebase for Pulse — a painless and affordable site & server monitoring tool designed for developers.

Today, I’m going to talk about test factories and how you can use them to make your tests more concise and easier to maintain.

The structure of a test

When it comes to writing a test, the way they’re structured tends to be like a story. You have a beginning in which you create the world, a middle where you have your characters tell a story, and an end where the results of the story are assessed and a lesson is learned.

To illustrate this, take a look at the following example test:

/** @test */
public function a_user_can_view_their_dashboard()
{
    // Beginning (define the world / create characters)
    $user = factory(User:class, 1)->create();

    // Middle (the characters go on a journey in the world)
    $response = $this->actingAs($user)->get("/account");

    // End (the outcome of the journey is assessed)
    $response->assertSee("My Account");
}

In the case of the above, the setup of the world is pretty simple. It just involves us creating a single user. However, many tests will require you to do a whole lot more than that before you can begin performing actions / asserting results.

You may need to create many records which are all connected. Often this can result in a test where the setup involves more code than the test itself. In my opinion, this makes the test harder to understand, so let’s look at some of the ways you can address this to make your tests shorter / easier to read.

Using setUp and tearDown

PHPUnit includes functionality that will automatically run a setUp method before a test, and tearDown method after a test, if it finds either one of these methods defined in the same class the test is in.

Therefore you could address the issue by doing something like this:

protected function setUp()
{
    // Create your database records
}

/** @test \*/
public function a_user_can_do_something()
{
    // Setup is already done

    $result = $user->performAction();

    $this->assertTrue($result);
}

However, there is a significant downside to this approach. The same setUp method will be called for every test you define in the class. As a result, you’d need to have one test per class if you wanted to use a different setUp routine.

As you’re probably realising, the setUp and tearDown methods are only really useful for shared behaviour e.g. purging any temporary files created by tests.

A secondary issue with this approach is that it is difficult to maintain. If you change the logic required to setup a test, then you’d have to manually go through all your tests and make the necessary revisions.

Fortunately, there’s a better way… test factories.

Creating test factories

There’s nothing complicated about what a test factory is. It’s simply a class that contains predefined methods you can use to populate a database exactly as you need it to be for a certain scenario.

Let’s examine this with an example from Pulse. When a user creates a new server monitoring profile, Pulse automatically adds records for key hardware components e.g. CPU, RAM, Disk Storage, System Load etc.

If we were to illustrate this setup in a test, it would look like this:

$server = factory(Server:class, 1)->create();

factory(Monitor:class, 1)->create(["key" => "cpu"]);
factory(Monitor:class, 1)->create(["key" => "ram"]);
factory(Monitor:class, 1)->create(["key" => "load"]);
factory(Monitor:class, 1)->create(["key" => "storage"]);

As you can imagine, Pulse includes many tests where it requires this kind of setup and there is always the possibility that it could change, so instead, let’s move this logic into a test factory like so:

class ServerFactory
{

    public $server;


    public function __construct($server)
    {
        $this->server = $server;
    }


    public static function createOne($attributes = [])
    {
        $server = factory(Server:class, 1)->create($attributes);

        return new ServerFactory($server);
    }


    public function withMonitors()
    {
        factory(Monitor:class, 1)->create(["key" => "cpu"]);
        factory(Monitor:class, 1)->create(["key" => "ram"]);
        factory(Monitor:class, 1)->create(["key" => "load"]);
        factory(Monitor:class, 1)->create(["key" => "storage"]);

        return $this;
    }

}

Now, within our test we can simply do the following:

$server = ServerFactory::createOne()->withMonitors()->server;

We now have a single class that defines the reusable logic to create a server with its standard monitors, and if we ever wanted to change how this was done, we’d only need to do it here.

To illustrate the visual improvement, consider the following before and after implementations of the same (abridged) test:

// ============
// == BEFORE ==
// ============

/** @test */
public function a_user_can_view_a_server()
{
    $user = factory(User:class, 1)->create();

    $server = factory(Server:class, 1)->create();

    factory(Monitor:class, 1)->create(["key" => "cpu"]);
    factory(Monitor:class, 1)->create(["key" => "ram"]);
    factory(Monitor:class, 1)->create(["key" => "load"]);
    factory(Monitor:class, 1)->create(["key" => "storage"]);

    $this->actingAs($user)
         ->json("POST", "/api/v1/servers/view/{$server->id}")
         ->assertSuccessful();
}

// ===========
// == AFTER ==
// ===========

/** @test */
public function a_user_can_view_a_server()
{
    $user = factory(User:class, 1)->create();
    $server = ServerFactory::createOne()->withMonitors()->server;

    $this->actingAs($user)
         ->json("POST", "/api/v1/servers/view/{$server->id}")
         ->assertSuccessful();
}

Instead of your eyes being drawn to all of the setup, you focus on the HTTP request and what it’s testing, which is the whole point :)

One thing to consider

Using test factories has absolutely no drawbacks, other than the fact you’re creating more code to setup your tests. As a result, I would suggest not always reaching for them by default. Rather consider them a useful tool for handling code repetition or especially long setup routines.

To illustrate my point, consider the following code:

$user = factory(User:class, 1)->create();

It makes no sense to create a factory that executes this single line of code to create one record. For this scenario, simply write the line of code in your test.

Wrapping Up

Hopefully you’ve seen how using test factories can be significantly beneficial in improving the overall readability of your tests, their maintainability, as well as speeding up how quickly you can write them.

I have more articles to share, so if you’re interested in reading them, be sure to follow me here on Medium. You can also follow me on Twitter.

Lastly, if you’re in the market for an affordable and painless site & server monitoring tool that doesn’t require you to have a DevOps degree, please take a moment to check out Pulse. I think you’ll find it to a be a breath of fresh air!

Thanks again and happy coding!


Posted on by:

mattkingshott profile

Matt Kingshott 👨🏻‍💻

@mattkingshott

Founder. Developer. Writer. Lunatic. Created Pulse, IodineJS, Axiom, and more. #PHP #Laravel #Vue #TailwindCSS

Discussion

markdown guide