Efficient testing with fixtures on Symfony 4

Data fixtures in Symfony 4 using AliceDataFixtures

Pablo Godinez
ManoMano Tech team
9 min readJun 27, 2019

--

Introduction — What are fixtures and why use them ?

Fixtures are used to load a “fake” set of data into a database that can then be used for testing or help give you some interesting data while you’re developing your application.
DoctrineFixturesBundle documentation

Fixtures is the term used to define a set of generated “fake” data that are mainly used for testing purposes. In PHP projects, fixtures are often used along with Behat or phpunit to ensure an initial state for test cases. This is what will be covered in this article, but they can also be used to generate a “dev” database in order to have a relevant set of data when you cannot use a prod dump (for any reason, like GDPR).

Using these, an application can be tested by actually sending SQL queries to a functioning database, removing the need to tediously mock repository classes with predefined behaviour
Andrew Carter

How to use fixtures the right way?

There are many ways to generate and load fixtures. A common mistake is to generate and load all fixtures at once then run all the tests. This is a really bad idea as the tests will depend on each other, some tests will rely on other previously ran tests to create the initial state making your tests fail if they are not run in the correct order. In the long term it becomes a nightmare to maintain.

Another popular solution is to generate all fixtures once at the beginning of the tests, cache (backup) the database and refresh the database for every test. This resolves the issue of tests having to run in a specific order to pass. But if several tests rely on the same fixtures, any modification of these fixtures (because a domain rule changed and some tests needs to change accordingly) may break other tests relying on the same data.

For these reasons it’s recommended to have a set of fixtures for each test. This means it’s not possible to launch a console command to load fixtures before running the test suites. To achieve this it’s necessary to load fixtures at runtime, before every test.

Getting Started — Tooling 🔨

The Symfony ecosystem has a lot of tools to load fixtures in many different ways.

Set up

Install dependencies

composer require --dev doctrine/data-fixtures # Data Fixtures Extension
composer require --dev theofidry/alice-data-fixtures # Will install nelmio/alice

Enable bundles

Edit config/bundles.php

Configure doctrine

This article shows the usage of an SQLite database for the tests.

They are 2 main reasons to use SQLite:

  • It’s fast.
  • It’s really easy to use everywhere. Particularly on the CI, having a dedicated MySQL or PostgreSQL database for each build requires a lot of configuration whereas Doctrine can create the sqlite file on the cache directory of each build so it requires almost zero intervention (you may need to install the sqlite driver and the php extension for sqlite).

But it also comes with cons:

  • SQLite does not support all features (eg. enums) that most of production RDBMS have so it requires to find workarounds.
  • It’s not 100% reliable. Bugs on the production RDBMS may be undetected by the tests running on SQLite.

But in the end, the database used for the test is up to you.

Create a config/packages/test/doctrine.yaml file with:

Doctrine may be configured to use multiple connections, if it’s the case then override each connections like so:

Note: you may want it use an in-memory sqlite database to speed up tests, but you may face issues with doctrine (see link for more info).

Create a FixtureContext for Behat

In Behat’s contexts directory (features/bootstrap/ by default) create a FixtureContext.php file.

The FixtureContext needs 3 things:

  • The doctrine registry, in order to create the sqlite database that matches our entities
  • A loader from theofidry/AliceDataFixtures to load and persist the fixtures
  • A directory to find the fixtures into (tests/Fixtures for example)

Take a look at
loader: '@fidry_alice_data_fixtures.doctrine.persister_loader' in behat.yml. It means we use the PersisterLoader from theofidy/alicedatafixtures. There are several loaders available:

  • SimpleLoader, that just takes fixture files and uses nelmio/alice to generate the fixtures
  • PersisterLoader, decorates a loader (LoaderInterface) and persists the objects (entities) generated from that loader
  • PurgerLoader, decorates a loader (LoaderInterface) and purges (using truncate or delete at choice) tables before calling the loader (PersisterLoader by default)

As told earlier, having dedicated fixtures for each test is important so we want the database to be empty before loading any fixture. So why not use the PurgerLoader ? Because Doctrine takes care of this part with:

Reminder: each behat context will be rebuilt (constructor called) at each scenario

A little warning at this point, SchemaTool is a REALLY dangerous class, as any bin/console doctrine:schema command is. No need to say that $schemaTool->dropDatabase() can result in a disaster if used on the wrong database. So be sure to prevent its execution and don’t use a database user with drop rights on your production server. Here is an example to prevent its execution:

Anyway, Behat is now ready to load some fixtures, but it needs something to test.

Testing context

The context is a dummy application: a REST api that manages products. Products are composed of a name, a price and are part of ProductCategory itself composed only of a name. Here are the Doctrine entities:

Now here’s the code of the action listing the products, they can be filtered by passing a “category” argument with the category name in the query string.

First behat scenarios

It requires 2 scenarios to cover this action:

  • Without filter (not entering the if block)
  • With a filter on category (entering the if).

To do this, we need to create 2 categories “tools” and “lighting” and create different numbers of products, let’s say 10 tools products and 5 lighting. So the first scenario should have all 15 products listed, then by filtering with the tools category it should only have 10 products.

Create a list-products.yaml in the fixtures path:

There are a few interesting things to say about this file:

First the root nodes App\Entity\ProductCategory and App\Entity\Product: it is the FQCN of the objects to create, in this case they are Doctrine entities but they could be ANY class. This node must be unique in a file, this means it’s not possible to write some Product fixture then a ProductCategory then again some Product, this is not a problem because fixture order does not matter.

Next level on the tree are the references category_tools, category_lighting, category_products_{1..10} and lightning_products_{1..5}. References are really useful to inject fixtures into other fixtures or to get the generated fixture to access its properties (more about that later).

The {1..10} notation for the product references, this is called a range (see the doc here). For tools_products_{1..10} it will create ten fixtures, each one having its own reference tools_products_1, tools_products_2, tools_products_3 … and so on.

Last level of the tree, the fixture’s properties. For ProductCategory nothing fancy it just sets the name property with a hardcoded string.
For Product, the name and price properties uses Faker’s formatters: firstName and randomFloat.There are tons of formatters available, the full list is in the documentation. It’s also possible to create our own formatters if needed. Finally, the category property uses a reference to a fixture. References are prefixed by @. Please note that the order in which the fixtures are written does not matter: in this case the referenced fixtures (ProductCategory) are written before the fixture that references them but it works anyway.

Now we have everything we need to write the behat scenarios:

These scenarios use steps from the Behatch/contexts package that provides some common Behat tests.
Now run Behat and voilà 🎉

Adding a Product —Retrieve fixture references & share them

We now want to be able to add products through the app. The products require a category when they are created, the client of the app will be required to pass the id of the category in the payload like so:

{
"name": "name of the product",
"price": 10.00,
"category: <category_id>
}

We’ll begin with the Behat scenario and fixture files first to take a TDD approach.

Begin by creating an empty Action:

To test that we properly added a product we need to check the number of products available before adding the new one, then add the new one, then check the total number of products again. It also requires that at least one category exists.

The fixture file and Behat scenario looks like this:

The important thing to notice here is the category id we pass in the payload of the POST request:"category": 1 . This test assumes that the id of the “tools” category will be 1, but nothing guarantees that. First, you could be using UUIDs in production (good idea BTW). Secondly, imagine this test grows because we add features and there are now several categories. If the tests rely on the database-generated (auto-incremented) ids and you have to add a category between 2 existing categories you’ll have to edit all tests.

Introducing SharingContext

To solve this problem we need to change the payload of the POST request dynamically to use the real id of the “tools” category. The category itself can be retrieved through its reference @category_tools. So we’d like to be able to do something like this

The issue here is how to share the value of “category_tools_id” between the 2 steps. Fortunately Behat provides a way to fix this issue. See: Accessing Contexts from each other. We’ll create a SharingContext that will be responsible to hold values that must be shared between our contexts.

Thank to twig templating (credits to Alexandre Salomé for the idea), it will be even easier to access and share the fixture properties like so:

We’ll also create a SharedContextTrait so Contexts can easily access the SharingContext.

The SharingContext implements the ArrayAccess interface so we can retrieve and set values like with an array:

We need to override the When I send a "POST" request to "/products/" with body: step to use the twig templating from the SharingContext

If we dump the request content in our Action and run Behat (don’t forget to add the newly created Contexts to behat.yaml) we can see that {{ category_tools.id }} has been replaced by 1:

Now that the Behat scenario is ready it is super easy and convenient to write the feature because it doesn’t require to manually send a POST request to the API but only to run the behat scenario until it passes.

Don’t do this on production

Conclusions

Fixtures are great to easily create the starting state of tests and to test database integration.

With Behat, the SharingContext and Twig Templating it’s really easy to write readable scenarios and the dynamic parts of the database are no problem.

In this post I overrode the RestContext to use fixtures in the payload of API calls (I also could have used them in the urls) but there are a lot more applications. I personally have a ProcessorContext with which I test swarrot processors by passing them message payloads with fixtures data, then I use a DatabaseContext to test how the entries have been modified by the Processor.

Alice is a really powerful lib, check the documentation, it’s almost impossible that it can’t fit your needs.

In the case you wanna play with this, the demo code can be found on github here: https://github.com/Zayon/fixtures-demo

License for this article is: Attribution 4.0 International (CC BY 4.0)
https://creativecommons.org/licenses/by/4.0/

--

--