Loading Fixtures in Symfony

by|inArticles||5 min read
Fixtures in Symfony<br>
Fixtures in Symfony<br>

In a previous article I already described how to set up a database for test. There we learned how to direct our testing environment to an empty database instead of the development database. We have managed to reset the test database before a test is started. This way we can always rely on a "clean" data-set when testing (for instance business logic).

Since an empty database is not of much and interest when testing business logic, we will create some Fixtures. Fixtures are sample datasets that we can define directly in our code (or in yaml by using additional Libraries).

Writing Fixtures in Symfony

To be able to write Fixtures in Symfony we need to ensure that we have the DoctrineFixturesBundle installed. If this bundle is not already install, we can install it by calling:

composer require --dev orm-fixtures

I will assume that we have a simple User Entity. A fixture for such a User Entity would look like this:

<?php

namespace App\DataFixtures\Doctrine;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class UserFixture extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $user = new User();
        $user->setName('Alexej');
        $manager->persist($user);

        $manager->flush();
    }
}

Loading Fixtures once before a Test Run

The first option that we have to load fixtures is to run them each time we run phpunit. Basically, we can run fixtures manually by running the following Symfony command from the doctrine library:

php bin/console doctrine:fixtures:load

To complete our testing setup from "" we can add this command to the bootstrap.php file. This way we will have a clean and populated database each time we run phpunit.

This is especially great when we rely on a certain state of the database in our tests and only read data to test business logic. But what if we modify the data? This would imply that each modification to our database has the potential to break other tests. We need to take this into account and be cautious in which order the tests are executed. This is usually a bad idea in software development.

Loading Fixtures before each Test

When modifying data in tests it is a good practice to return the database to a known state after running a test that performs modifications. We can achieve this in Symfony in different ways. I will describe here the way I prefer to do it and list some alternatives afterwards.

For our reproducible tests I will use the LiipTestFixturesBundle. This will help us to run Fixtures in our tests. You can install it by running the following composer command:

composer require --dev liip/test-fixtures-bundle:^2.0.0

As well you will need to add the following class to your bundles configuration:

Liip\TestFixturesBundle\LiipTestFixturesBundle::class

This bundle will enable us to load Fixtures in test. There are ways to load Fixtures without using this bundle but in the end you will re-write code that is already present in this bundle.

To be able to run our fixtures we need a common test class that will represent our testcase that performs modifications and therefore requires a fixture load. I will call this class FixtureAwareTestCase:

<?php

namespace App\Tests;


use App\DataFixtures\Doctrine\UserFixture;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

abstract class FixtureAwareTestCase extends WebTestCase
{
    protected KernelBrowser $client;

    public function setUp(): void
    {
        // Will boot the kernel and give us a new client for testing
        $this->client = static::createClient();

        $doctrine = static::getContainer()->get('doctrine');
        $this->entityManager = $doctrine->getManager();

        $this->loadFixtures();
    }

    public function tearDown(): void
    {
        $this->truncateDatabase();
        parent::tearDown();
        unset($this->entityManager);
    }

    private function loadFixtures(): void
    {
        $databaseTool = static::getContainer()->get(DatabaseToolCollection::class)->get();

        $databaseTool->loadFixtures([
            UserFixture::class
        ]);
    }

    private function truncateDatabase(): void
    {
        $ormPurger = new ORMPurger($this->entityManager);
        $ormPurger->purge();
    }
}

There are some things going on that we need to talk about. First of all, the setUp method will spin up the kernel for the application so we can use containers and managers. Afterwards we call the private method "loadFixtures". I extracted this part in its own method to make it more visible in the setUp method.

The loadFixtures method is using the DatabaseToolCollection::class from the Liip Bundle to load Fixtures. Here we can pass a list of fixture classes that we would like to be loaded.

Finally, there is a tearDown method which will be run after each test completed its execution. Here, we can use the default ORMPurger from the Doctrine Bundle which will simply remove all entities in our database.

This structure gives us the opportunity to run tests that can modify data in our database. And we can be sure that the database returns to it's well defined state each time we finish a test.

Alternative Ways For Fixture Loading

We have seen one way to populate/purge database entities for each test run. It brings a lot of advantages for testing. But those advantages are not "for free". Your tests will slow down, since we are removing and re-populating the database for each test case. This can be very time consuming, especially when the test database grows over time.

There are some attempts to overcome those slowdowns. For instance, we could populate the database once and then start a transaction before each test case. As soon as our test with modifications has run, we can simply rollback our transaction. This works pretty well for some scenarios. But in the scenarios where you are explicitly using transactional code in your business logic, this would collide.

Another attempt is to work with local files. Again, we populate the database once, save a copy of this database in another folder and run our tests. As soon as a test finishes, we can overwrite the data of the database file with the content we copied away. This is for sure faster than purging and populating the database. But, as well a bit "hacky".

Thank you for reading this far! Let’s connect. You can @ me on Twitter (@debilofant) with comments, or feel free to follow. Please like/share this article so that it reaches others as well.

Related Articles

© Copyright 2023 - ersocon.net - All rights reservedVer. 388