Fixtures, the right gestures

🔥 Fixtures recently helped me a lot to save time and write efficient tests.

In this article, I would like to share with you how fixtures can help to:

  • Test the performance of your app.
  • Generate awesome data on a Symfony project.
  • Apply good practices in functional testing.

This article explores ways to use Alice Bundle. It is a layer on top of Nelmio/Alice, the fixtures generator presented in this previous article.

I need data, now

I am developping a platform for a recruiting company, which enroles candidates in a very new program. The platform allows its administrators to check candidates profiles. It is in construction, it looks great. Except that I am not sure of its performance: will it be able to handle the hundreds of candidate profiles, as expected by the owner of the project?

To make sure of this, my goal is to insert into the database a great number of data.

My database is composed of 3 tables:

  • candidates: store the personal information of the candidates
  • skills: store the skills that may be attached to the candidates
  • candidate_skills: each candidate may have many skills, with a different rating for each skill. This table stores the many to many relations between the candidates and the skills.

So I wanted to insert into the database the following number of objects, with the following fields:

  • 200 candidates profiles (bearing a firstname, a lastname, an email and a job)
  • 500 skills (with a label)
  • 6000 candidate_skills (containing a skill, a candidate and a level: a number between 0 and 3), as we want each candidate to bear 30 skills in average.

A solution very not pragmatic

Ok so let’s just insert that in the database !

To be able to deal with these nested objects (objects related to each other), I need to specify the id of each entity.

INSERT INTO candidates (id, firstname, lastname, email, job) VALUES (1, 'Mario', 'Bros', 'mario@bros.com', 'Plumber')
INSERT INTO skills (id, label) VALUES (1, 'screwpooling')
INSERT INTO candidateSkills (id, skill_id, candidate_id, level) VALUES (1, 1, 1, 3)

This makes one candidate with one skills. Good, but I want 200 with 30 skills each. Of course, I could not repeat these lines because I needed the ids to increase.

So I wrote a script in Python to generate the lines to copy-paste in my sql console. Which took me a lot of time. In the end, this solution turned out to be:

❌ Not flexible.

The fact that the relation is handled through id reference makes every modification a nightmare. You often need to drop the whole data and regenerate them again

❌ Not easily scalable.

A few weeks later, I had to do the same tests. But the app had grown and there was many more entities to deal with. So I changed my strategy and I found something that changed my life: fixtures.

Fixtures, what is it?

🎭 Fake data used to test an application.

Fake in the way that it is not real production data. It is generated on the purpose of testing your app, such as its performance (how fast the page loads) or its functionalities(displaying a list for example).

📂 Data described in files inside your project repository

🖌 Data generated and inserted into the database with a single command line

How to generate awesome fixtures?

Alice Bundle

My life changed when I discovered this great bundle: Hautelook Alice Bundle. It makes it very easy to handle the relations between objects.

Intall the bundle with composer:

composer require hautelook/alice-bundle

After installing the bundle, just describe in a .yaml file the fixtures you want to add to your database:

# api/fixtures/candidate.yaml
App\Entity\Candidate:
    candidate_{1..200}:
       firstname: <firstName()>
       lastname: <lastName()>
       email: <email()>
       company: <company()>
       job: <jobTitle()>
# api/fixtures/skill.yaml
App\Entity\Skill:
    skill_{1..100}:
       label: <jobTitle()>
# api/fixtures/candidate_skill.yaml
App\Entity\CandidateSkill:
    candidate_skill_{1..6000}:
       candidate: '@candidate_<numberBetween(1,200)>'
       skill: '@skill_<numberBetween(1,100)>'
       level: '<numberBetween(0,3)>'

Then simply generate the fixtures and populate your database with this command:

php bin/console hautelook:fixtures:load

You will be asked: Careful, database will be purged. Do you want to continue y/N?

If you don’t want it to be purged you can append the option –append to the above command.

TADAAAAA: your database is populated with real looking data !

candidates table

skill candidates table

So this bundle allows you to easily:

✅ Scale the number of fixtures

Use the 'object_name_{1..x}' notation to generate x number of fixtures. Each one of them will have a different variable name (candidate_1, candidate_2, …) you will be able to refer to.

✅ Handle nested objects

Use the '@related_object_name' notation to refer to another fixture named “related_object”. In the example above, any talent_skill_i will be related to the fixtures candidate_j and skill_k where i, j and k are random numbers.

You don’t have to matter about the order in which the fixtures are built, the bundle will handle this for you.

✅ Build real looking fake data

Use the <fake_attribute()> notation to generate a random string of the type you want. For example <jobTitle()> may give you funny things such as:

  • Upholsterer
  • Gaming Supervisor
  • Stone Cutter

Take the look at the awesome Faker library used Alice bundle. A lot of parameters are customisable such as the language.

✅ Set up your database

Populate your database with one command to allow any developer to quickly set up a local database.

Test your code with fixtures

Fixtures are also a good way to write great functional tests. Indeed, as mentioned in the doc of phpunit:

“A fixture describes the initial state your application and database are in when you execute a test.”

As recommanded by Symfony, Alice Bundle is the best practice when your tests deal with database entities.

Indeed, it allows you to load a specific fixture file for each test.

An example: functional testing of a controler

We want to test the controler that gets the candidates of your database. Follow these steps:

  1. Write a fixture file “get_candidates.yaml”. It describes the state of your database when you will exectute the test.
# tests/fixtures/get_candidates.yaml
App\Entity\Candidate:
    candidate1:
       firstname: francis
       job: plumber
    candidate2:
       firstname: romeo
       job: zoo keeper
  1. In you test file, load this fixture specifically.
  1. Call the controler that gets the candidates.
  1. ✔ Check that it returns 2 candidates (with names francis and romeo).
// tests/Controller/CandidateControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Response;

class CandidateControllerTest extends WebTestCase
{
    private $client;

    public function setUp()
    {
        // in your phpunit configuration, make sure this purges the database
        $this->client = static::createClient();

        // get Alice fixture loader
        $loader = static::$kernel->getContainer()->get('fidry_alice_data_fixtures.loader.doctrine');

        // load the fixture needed and only this one
        $loader->load(['tests/fixtures/get_candidates.yaml']);
        static::loadFixtures('get_candidates.yaml');
    }


    public function testGetCandidates()
    {
        $this->client->request('GET', '/api/candidates');

        $responseBody = json_decode($this->client->getResponse()->getContent(), true);

        $this->assertEquals(2, count($responseBody['content']));
        $this->assertEquals('francis', $responseBody['content'][0]['firstname']);
    }
}

Tests independency

It is really important to make sure your tests are independant. In my GET /candidates test, I don’t want to be bothered by other data used in other tests.

This could make my test fail if I get more than 2 candidates.

Or worse, it could hide a default in my code. Let’s imagine my controler does not pick in the candidates table but in the agents table. If I had previously loaded 2 agents with the same firstnames, my test would have passed…

In conclusion, it is necessary to:

Purge the database before each test

This is handled by the loader "fidry_alice_data_fixtures.loader.doctrine". Before each test function, it purges the database and then loads your fixtures.

loader console logs

Purging the database may be costly in time. I am currently looking for a way to load the fixtures in a transaction, as suggested in the documentation of AliceDataFixtures (the library used by Alice Bundle to persist the loaded objects).

✅ Make sure your tests are independent

Describe a specific fixture file for each test and load it in your setUp() thanks to Alice loader.

Conclusion

Fixtures are a great tool to:

⏱ Save time on the installation of a project that uses a database

🚀 Test the performance of your app

🎭 Have fun with the Faker

With the awesome Hautelook Alice Bundle you can:

🎼 Easily generate fixtures on a Symfony project

🗂 Load a specfic fixture file for each test

✔ Guarantee test independency


Why not Doctrine Fixtures Bundle?

To generate fixtures on a Symfony project, I first tried Doctrine Fixtures Bundle:

// src/DataFixtures/AppFixtures.php
namespace App\DataFixtures;

use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;

class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        // create 200 candidates!
        for ($i = 0; $i < 200; $i++) {
            $candidate = new Candidate();
            $candidate->setFirstname('first name '.$i);
            $candidate->setJob('Job '.$i);
            $manager->persist($candidate);
        }

        $manager->flush();
    }
}

✅ Describe your objets in a file
✅ Load these objects by executing a command line
❌ Easily deal with the nested objects

The bundle provides a way to add reference to objects, to be able to make a relation between your fixtures, but it is quite painful to do. Moreover, you have to think about the order in which the fixtures will be loaded.