Fixturesy W Testach Aplikacji Symfony

programit.pl 3 lat temu

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-pack

Dodatkowo będziemy potrzebować SymfonyMakerBundle, który umożliwia generowanie klas testów, kontrolerów, migracji, itp.

composer require --dev symfony/maker-bundle

Instalacja DoctrineFixturesBundle odbywa się poprzez wywołanie komendy:

composer require orm-fixtures --dev

Potrzebujemy też PHPUnit'a do pisania testów:

composer require --dev phpunit/phpunit symfony/test-pack

Na koniec instalujemy LiipTestFixturesBundle:

composer require liip/test-fixtures-bundle --dev

Przykł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.

<?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]; // } }

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()); } }
Idź do oryginalnego materiału