Effective tests: Creating test data with fixture factories

Written by Dave Marshall - - Aggregated on Wednesday November 11, 2015

Following from my post on setting up a database fixture for your test suite, the next step is adding data to that fixture for your specific tests. The more specific Arrange part of the Arrange, Act, Assert pattern.

For a long time, I thought the only way to have database records for my tests, was to manage one large sql dump that contained lots of records, all of which were required for one or more tests within the test suite, or to use DBUnit with a bunch of XML files. This changed when I came across factory_girl in some ruby test suites.

There are a bunch of similar packages for php out there (factory-muffin springs to mind), but I've always tended to roll my own, mostly as I've worked in gnarly legacy code bases and to be honest, it's not really the most complicated thing to do. I also keep them as simple as possible and avoid the production code, so data gets inserted directly in to the database, rather than using an ORM to store the data. I've previously written about Object Mothers and Test Data Builders, which I would use alongside the ORM if I wanted to do things that way.

If you're generating a lot of fake data, you might want to look in to using something like Faker, but I've found a handful of simple functions cater for most of my needs.

As a starting point, given a users table with an email and password field, I'll add the schema to the fixture.sql file as mentioned in the setting up a database fixture article, then create a class like this:

<?php

namespace tests\support;

use Doctrine\DBAL\Connection;

class UserFixtureFactory
{
    private $conn;

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

    public function create()
    {
        $data = [
            'email' => "user@example.org",
            "password" => password_hash("password", PASSWORD_DEFAULT),
        ];

        $this->conn->insert('users', $data);
    }
}

This seems simple enough and is easy enough to get working in one of our tests.

    /** @test */
    public function fixture_factory_works()
    {
        $userFixtureFactory = new UserFixtureFactory($this->conn());
        $userFixtureFactory->create();

        $this->assertEquals(1, $this->conn()->fetchColumn("SELECT COUNT(*) FROM users"));
    }

Like any good users table, our email field has a unique constraint on it, so we need to work around that:

    /** @test */
    public function fixture_factory_works_with_lots_of_users()
    {
        $userFixtureFactory = new UserFixtureFactory($this->conn());

        for($i = 0; $i < 10; $i++) {
            $userFixtureFactory->create();
        }

        $this->assertEquals(10, $this->conn()->fetchColumn("SELECT COUNT(*) FROM users"));
    }

Adding a simple counter to the method will keep our email addresses unique:

    public function create()
    {
        static $counter = 0;
        $counter++;

        $data = [
            'email' => "user{$counter}@example.org",
            "password" => password_hash("password", PASSWORD_DEFAULT),
        ];

Yay green test! It's kinda slow though, slower than I expected. Probably the password hashing, let's fix that value with a literal.

        $data = [
            'email' => "user{$counter}@example.org",
            "password" => '$2y$10$Fx9LBid2/HV24SseoTp/sulorRnkykwN7D8HbUvsIgPtrDsxBqnUq', # password_hash("password", PASSWORD_DEFAULT),
        ];

The next thing I want is to allow the caller to override the default data:

    /** @test */
    public function fixture_factory_allows_overriding_defaults()
    {
        $userFixtureFactory = new UserFixtureFactory($this->conn());

        $userFixtureFactory->create(['email' => 'dave@example.org']);

        $this->assertEquals('dave@example.org', $this->conn()->fetchColumn("SELECT email FROM users"));
    }

    public function create(array $data = [])
    {
        static $counter = 0;
        $counter++;

        $data = array_merge([
            'email' => "user{$counter}@example.org",
            "password" => '$2y$10$Fx9LBid2/HV24SseoTp/sulorRnkykwN7D8HbUvsIgPtrDsxBqnUq', # password_hash("password", PASSWORD_DEFAULT),
        ], $data);

        $this->conn->insert('users', $data);
    }

Finally, I want the factory to return the data it used, so that the test code can make use of it as necessary:

    /** @test */
    public function fixture_factory_returns_data()
    {
        $userFixtureFactory = new UserFixtureFactory($this->conn());

        $id = $userFixtureFactory->create()['id'];

        $this->assertEquals($id, $this->conn()->fetchColumn("SELECT id FROM users"));
    }

    public function create(array $data = [])
    {
        static $counter = 0;
        $counter++;

        $data = array_merge([
            'email' => "user{$counter}@example.org",
            "password" => '$2y$10$Fx9LBid2/HV24SseoTp/sulorRnkykwN7D8HbUvsIgPtrDsxBqnUq', # password_hash("password", PASSWORD_DEFAULT),
        ], $data);

        $this->conn->insert('users', $data);

        $data['id'] = $this->conn->lastInsertId();

        return $data;
    }

That's pretty much it.

All the test examples so far have created the fixture factory when required, I don't recommend doing this and would probably create a helper method as a trait or on a base class.

    /** @test */
    public function fixture_factory_works()
    {
        $this->hasAUser();

        $this->assertEquals(1, $this->conn()->fetchColumn("SELECT COUNT(*) FROM users"));
    }

    public function hasAUser(array $data = [])
    {
        $userFixtureFactory = new UserFixtureFactory($this->conn());

        return $userFixtureFactory->create($data);
    }

Things tend to get more complicated than this, particularly when your factories need to be aware of other factories, in order to create and maintain relationships. I'll cover how I tackle that in another article, but needless to say, it's not much different from managing dependencies in your production code.

Happy testing!


« Verifying Doubles in PHP - Dave Marshall

Dave Marshall - Effective tests: Setting up a database … »