Jakiś czas temu tworzyłem nową aplikację w Symfony. Jako ORM użyłem Doctrine. Pisałem też testy, w których używałem bazy danych, aby sprawdzić, czy komponenty prawidłowo ze sobą współdziałają i dane wyszukują się w prawidłowy sposób. Potrzebowałem narzędzia, które wypełni bazę danych przykładowymi danymi, aby nie trzeba było tworzyć ich za każdym razem i aby były takie same we wszystkich testach.
Doskonałym narzędziem do tego celu okazał się DoctrineFixturesBundle. Umożliwia tworzenie przykładowych danych, które mogą być później wykorzystane w testach. Dane można tworzyć w jednym pliku lub podzielić je, np. według encji. Bundle obsługuje bazy danych takie jak MySQL, PostgreSQL czy SQLite. Warto dodać, iż fixturesy mogą być wykorzystywane nie tylko w testach - można ich np. użyć do wypełnienia bazy deweloperskiej przykładowymi danymi.
Aby załadować Fixturesy do testowej bazy danych, wykorzystałem narzędzie LiipTestFixturesBundle. Umożliwia ono pisanie testów funkcjonalnych. Zawiera też serwisy pozwalające na ładowanie danych, o których mowa wyżej.
Instalacja zależności
Zakładając, iż zaczynamy nasz projekt od początku, będziemy potrzebować kilku paczek. Na początku zainstalujmy Doctrine. Robimy to poleceniem:
composer require symfony/orm-packDodatkowo będziemy potrzebować SymfonyMakerBundle, który umożliwia generowanie klas testów, kontrolerów, migracji, itp.
composer require --dev symfony/maker-bundleInstalacja DoctrineFixturesBundle odbywa się poprzez wywołanie komendy:
composer require orm-fixtures --devPotrzebujemy też PHPUnit'a do pisania testów:
composer require --dev phpunit/phpunit symfony/test-packNa koniec instalujemy LiipTestFixturesBundle:
composer require liip/test-fixtures-bundle --devPrzykładowe encji
W tak przygotowanej aplikacji stwórzmy przykładowe encje, których użyjemy w testach.
<?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; } //... }Dodanie fixturesów
Fixturesy tworzymy w klasach, które rozszerzają klasę Fixture. Możemy dodać tu przykładowe encje i zapisać je używając EntityManagera.
Możemy też dodać referencje do tak stworzonych encji - wtedy będziemy mogli użyć ich w innych klasach fixturesów oraz w testach.
Co więcej, jeżeli nasza klasa będzie implementowała interfejs DependentFixtureInterface, będziemy w stanie podać, od jakich fixturesów jest ona zależna.
Testowanie
Testy powinny rozszerzać klasę KernelTestCase. Umożliwi to skorzystanie w nich z bazy danych. Dodawanie fixturesów jest teraz banalnie proste. Wystarczy na serwisie DatabaseToolCollection wywołać metodę loadFixtures, która jako argument przyjmuje tablicę nazw klas.
<?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()); } }