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
Additionally, we will need a SymfonyMakerBundle that allows you to generate predefined test classes, controllers, migrations, etc.
composer require --dev symfony/maker-bundle
DoctrineFixturesBundle installation is done by running the command:
composer require orm-fixtures --dev
We also need PHPUnit to write tests:
composer require --dev phpunit/phpunit symfony/test-pack
Finally, we install the Liip Test Fixtures Bundle:
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
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
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
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
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());
}
}
Top comments (1)
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