Using Fixtures In Testing Symfony Application
Tags: php, symfony, tutorial, phpunit
Published: 2021-08-26 15:47:12


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:
```shell
composer require symfony/orm-pack
```

Additionally, we will need a SymfonyMakerBundle that allows you to generate predefined test classes, controllers, migrations, etc.
```shell
composer require --dev symfony/maker-bundle
```

DoctrineFixturesBundle installation is done by running the command:
```shell
composer require orm-fixtures --dev
```

We also need PHPUnit to write tests:
```shell
composer require --dev phpunit/phpunit symfony/test-pack
```

Finally, we install the Liip Test Fixtures Bundle:
```shell
composer require liip/test-fixtures-bundle --dev
```

# Example Entities
In such an application, let's create example entities that we will use in tests.
```php
<?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;
    }
    
    //...
}
```

```php
<?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;
    }
    
    //...
}
```
# 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
<?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];
//    }
}
```
# 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
<?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());
    }
}
```