DEV Community

Jarek
Jarek

Posted on

Using Fixtures In Testing Symfony Application

Recently I was creating a Symfony application. I used Doctrine as the ORM. I also wrote tests in which I used a database to check that the components were properly interacting with each other and that the data was fetched correctly. I needed a tool which would populate the database with sample data so that I didn't have to create them every time and that they would be the same in all tests.

DoctrineFixturesBundle turned out to be an excellent tool for this purpose. It enables the creation of sample data that can later be used in tests. Data can be created in one file or divided, e.g. by entity. The bundle supports many databases such as MySQL, PostgreSQL or SQLite. What is more, fixtures can be used not only in tests - they can, for example, be used to fill the development database with sample data.

I also used the LiipTestFixturesBundle. This tool includes services which would load fixtures into the test database. It allows writing functional tests as well.

Installing Dependencies

Assuming we start our project from scratch, we will add a few packages. First, let's install Doctrine. We do this with the command:

composer require symfony/orm-pack
Enter fullscreen mode Exit fullscreen mode

Additionally, we will need a SymfonyMakerBundle that allows you to generate predefined test classes, controllers, migrations, etc.

composer require --dev symfony/maker-bundle
Enter fullscreen mode Exit fullscreen mode

DoctrineFixturesBundle installation is done by running the command:

composer require orm-fixtures --dev
Enter fullscreen mode Exit fullscreen mode

We also need PHPUnit to write tests:

composer require --dev phpunit/phpunit symfony/test-pack
Enter fullscreen mode Exit fullscreen mode

Finally, we install the Liip Test Fixtures Bundle:

composer require liip/test-fixtures-bundle --dev
Enter fullscreen mode Exit fullscreen mode

Example Entities

In such an application, let's create example entities that we will use in tests.

<?php
declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;


#[ORM\Entity]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    #[ORM\Column(type: 'integer')]
    private ?int $id;

    #[ORM\Column(type: 'string', nullable: false)]
    private string $name;

    #[ORM\Column(type: 'integer', nullable: false)]
    private int $price;

    #[ORM\ManyToOne(targetEntity: Category::class)]
    #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id')]
    private Category $category;

    public function __construct(string $name, int $price, Category $category)
    {
        $this->name = $name;
        $this->price = $price;
        $this->category = $category;
    }

    //...
}
Enter fullscreen mode Exit fullscreen mode
<?php
declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;


#[ORM\Entity]
class Category
{
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    #[ORM\Column(type: 'integer')]
    private ?int $id;

    #[ORM\Column(type: 'string', nullable: false)]
    private string $name;

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

    //...
}
Enter fullscreen mode Exit fullscreen mode

Creating Fixtures

We create fixtures in classes which extend the Fixture class. We can add sample entities here and save them using EntityManager.
We can also add references to such created entities - then we can use them in other fixtures classes.
Moreover, if our class implements the DependentFixtureInterface interface, we will be able to specify which fixtures it depends on.

<?php

namespace App\DataFixtures;

use App\Entity\Category;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager;

class CategoryFixtures extends Fixture //implements DependentFixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $categories = ['Books', 'Sport'];

        foreach ($categories as $categoryName) {
            $category = new Category($categoryName);
            $manager->persist($category);
            $manager->flush();

            $this->addReference(sprintf('category-%s', $categoryName), $category);
        }
    }

//    public function getDependencies(): array
//    {
//        return [OtherFixtures::class];
//    }
}
Enter fullscreen mode Exit fullscreen mode

Testing

Tests should extend the KernelTestCase class. This will allow us to use database in them. Adding fixtures is now very easy. We can just call the loadFixtures method on the DatabaseToolCollection service, which takes an array of class names as an argument.

<?php

declare(strict_types=1);

namespace App\Tests;

use App\DataFixtures\CategoryFixtures;
use App\Entity\Category;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Liip\TestFixturesBundle\Services\DatabaseToolCollection;
use Liip\TestFixturesBundle\Services\DatabaseTools\AbstractDatabaseTool;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class ProductTest extends KernelTestCase
{
    protected AbstractDatabaseTool $databaseTool;
    protected EntityManagerInterface $entityManager;

    public function setUp(): void
    {
        parent::setUp();

        $this->databaseTool = self::getContainer()->get(DatabaseToolCollection::class);
        $this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
    }

    public function testChangeProductPrice(): void
    {
        $this->databaseTool->loadFixtures([
            CategoryFixtures::class
        ]);

        $category = $this->entityManager->getRepository(Category::class)->findOneBy(['name' => 'Books']);


        $product = new Product('Title', 100, $category);
        $this->entityManager->persist($product);
        $this->entityManager->flush();

        $this->entityManager->clear();

        $products = $this->entityManager->getRepository(Product::class)->findAll();

        self::assertCount(1, $products);

        /** @var $product Product */
        $product = array_shift($products);

        self::assertEquals(100, $product->getPrice());
        self::assertEquals('Title', $product->getName());
        self::assertEquals('Books', $product->getCategory()->getName());
    }
}
Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Collapse
nicohaase profile image
Nico Haase

Keep in mind that loading fixtures within the test is a huge performance bottleneck. If all tests share the same set of fixtures, its way easier to load them before the first test and use something like dama/doctrine-test-bundle that starts a transaction before each test and rolls it back afterwards