Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Używanie fixtures w testach w Symfony 4

Pablo Godinez Backend Developer / Evaneos
Sprawdź, jak wykorzystać data fixtures w Symfony 4, używając Doctrine i AliceDataFixtures w testach Behat.
Używanie fixtures w testach w Symfony 4

Fixture’y służą do ładowania „fałszywego” zestawu danych do bazy danych, który można następnie wykorzystać do testowania lub pomóc w uzyskaniu potrzebnych danych podczas tworzenia aplikacji. dokumentacja DoctrineFixturesBundle

Fixture to termin używany do zdefiniowania zestawu wygenerowanych „fałszywych” danych, które są używane głównie do celów testowych. W projektach PHP fixture’y są często używane wraz z Behat lub phpunit, aby zapewnić stan początkowy dla testowanych przypadków. Ten właśnie przykład użycia omówię w tym artykule, ale można ich również użyć do wygenerowania bazy danych „dev”, aby mieć odpowiedni zestaw danych, gdy nie możesz użyć prod dump (z jakiegokolwiek powodu).

Dzięki temu można przetestować aplikację, wysyłając zapytania SQL do działającej bazy danych, eliminując potrzebę żmudnego mockowania repozytoriów predefiniowanymi zachowaniami. Andrew Carter


Jak poprawnie używać fixture’ów?

Istnieje wiele sposobów generowania i ładowania fixture’ów. Częstym błędem jest generowanie i ładowanie wszystkich fixture’ów jednocześnie, a następnie uruchamianie wszystkich testów stałej kolejności. To naprawdę kiepski pomysł, ponieważ testy będą od siebie zależeć. Niektóre testy będą polegały na innych wcześniej przeprowadzonych, aby mogły stworzyć stan początkowy. Tak się jednak nie stanie u to spowoduje, że testy zakończą się niepowodzeniem, jeśli nie zostaną uruchomione w prawidłowej kolejności. Na dłuższą metę staje się to koszmarem do utrzymania.

Innym popularnym rozwiązaniem jest generowanie wszystkich fixture’ów raz na początku testów, tworzenie kopii zapasowej i odświeżanie bazy danych dla każdego testu. To rozwiązuje problem testów, które muszą zostać uruchomione w określonej kolejności. Ale jeśli kilka testów opiera się na tych samych fixture'ach, każda ich modyfikacja (ponieważ zmieniło się coś w domenie, testy też muszą się zmienić), może zepsuć inne testy oparte na tych samych danych.

Z tych powodów zaleca się posiadanie zestawu fixture’ów dla każdego testu. Oznacza to, że nie można uruchomić polecenia console, aby załadować fixture’y przed uruchomieniem pakietów testowych. Aby to osiągnąć, konieczne jest ładowanie fixture’ów w czasie wykonywania, przed każdym testem.


Na początek — narzędzia 🔨

Ekosystem Symfony ma wiele narzędzi do ładowania fixture’ów na wiele różnych sposobów.

  • Doctrine zawiera rozszerzenie Data Fixtures, aby ułatwić zapisywanie wygenerowanych fixture'ów.
  • nelmio/alice pozwala naprawdę łatwo generować fixture’y, zapisująć pliki php, yaml lub json. Opiera się na fzaninotto/Faker.
  • Wreszcie theofidry/AliceDataFixtures tworzy pomost między wygenerowanymi fixture’ami Alice a rozszerzeniem fixture z Doctrine, aby je utrwalić.


Konfiguracja

Instalacja zależności

composer require --dev doctrine/data-fixtures # Data Fixtures Extension
composer require --dev theofidry/alice-data-fixtures # Will install nelmio/alice



Włącz bundles

Edituj config/bundles.php:

<?php 
return [
    // ...
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], // should already be enabled.
    // ..
    Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['test' => true],
    Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['test' => true],
];


Konfiguruj doctrine

Ten artykuł pokazuje użycie bazy danych SQLite do testów. Są 2 główne powody, dla których warto używać SQLite:

  • Jest szybki.
  • Jest naprawdę łatwy w użyciu praktycznie wszędzie. Szczególnie w CI, posiadanie dedykowanej bazy danych MySQL lub PostgreSQL dla każdego builda wymaga sporo konfiguracji, podczas gdy Doctrine może utworzyć plik sqlite w katalogu pamięci podręcznej każdej kompilacji, więc prawie nie wymaga interwencji (konieczne może być zainstalowanie sterownika sqlite i rozszerzenie php dla sqlite).


Ale ma też wady:

  • SQLite nie obsługuje wszystkich funkcji (np. enumów), które ma większość produkcyjnych RDBMS, więc trzeba to jakoś obejśc.
  • Nie jest w 100% niezawodny. Błędy w produkcyjnych RDBMS mogą nie zostać wykryte przez testy działające na SQLite.


Ostatecznie wybór baza danych użyty do testu należy do Ciebie.

Utwórz plik config/packages/test/doctrine.yaml z:

doctrine:
dbal:
driver: pdo_sqlite
url: 'sqlite:'
path: "%kernel.cache_dir%/test_db.sqlite"


Doctrine może obsługiwać wiele połączeń jednocześnie i w takim przypadku zastąp wszystkie połączenia w następujący sposób:

doctrine:
dbal:
connections:
default_connection: default
# Override each connection
default:
driver: pdo_sqlite
url: 'sqlite:'
path: "%kernel.cache_dir%/test_db.sqlite"

secondary:
driver: pdo_sqlite
url: 'sqlite:'
path: "%kernel.cache_dir%/secondary_test_db.sqlite"


Uwaga: możesz użyć bazy danych sqlite w pamięci, aby przyspieszyć testy, jednak możesz napotkać problemy z doctrine.

Utwórz FixtureContext dla Behat

W katalogu contexts Behat (features/bootstrap/by default) utwórz plik FixtureContext.php.

<?php
declare(strict_types=1);
use Behat\Behat\Context\Context;
class FixtureContext implements Context
{
    // Empty for now
}


FixtureContext potrzebuje 3 rzeczy:

  • Registry z Doctrine, w celu utworzenia bazy danych, która pasuje do naszych encji
  • Loadera wczytującego z theofidry/AliceDataFixtures do wczytywania i zapisywania fixture’ów
  • Katalogu, w którym można znaleźć fixture’y (np. tests/fixtures)

 

default:
suites:
default:
contexts:
# other contexts
# ....
- FixtureContext:
doctrine: '@doctrine'
loader: '@fidry_alice_data_fixtures.doctrine.persister_loader'
fixturesBasePath: '%paths.base%/tests/Fixtures/'

# We need Behat's Symfony2 extension to be able to inject Symfony services
# composer require --dev behat/symfony2-extension
extensions:
Behat\Symfony2Extension:
kernel:
class: App\Kernel

 

<?php
declare(strict_types=1);
use Behat\Behat\Context\Context;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Fidry\AliceDataFixtures\LoaderInterface;
class FixtureContext implements Context
{
    /** @var LoaderInterface */
    private $loader;
    
    /** @var string */
    private $fixturesBasePath;
    
    /** 
     * @var array Will contain all fixtures in an array with the fixture
     *            references as key 
     */
    private $fixtures;
    
    public function __construct(
        Registry $doctrine,
        LoaderInterface $loader,
        string $fixturesBasePath
    ) {
        $this->loader = $loader;
        $this->fixturesBasePath = $fixturesBasePath;
        
        /** @var ObjectManager[] $managers */
        $managers = $doctrine->getManagers(); // Note that currently, 
                                              // FidryAliceDataFixturesBundle
                                              // does not support multiple managers
        foreach ($managers as $manager) {
            if ($manager instanceof EntityManagerInterface) {
                $schemaTool = new SchemaTool($manager);
                $schemaTool->dropDatabase();
                $schemaTool->createSchema($manager->getMetadataFactory()->getAllMetadata());
            }
        }
    }
    
    /**
     * @Given the fixtures file :fixturesFile is loaded
     */
    public function theFixturesFileIsLoaded(string $fixturesFile): void
    {
        $this->fixtures = $this->loader->load([$this->fixturesBasePath.$fixturesFile]);
    }
}


Spójrz na to:

loader: '@fidry_alice_data_fixtures.doctrine.persister_loader' w behat.yml. Oznacza to, że używamy PersisterLoader z theofidy/alicedatafixtures. Dostępnych jest kilka narzędzi wczytujących:

  • SimpleLoader, który po prostu pobiera pliki fixture’ów i używa nelmio/alice do generowania fixture’ów
  • PersisterLoader, dekoruje narzędzie ładujące (LoaderInterface) i zapisuje obiekty (encje) wygenerowane przez nie
  • PurgerLoader, dekoruje narzędzie ładujące (LoaderInterface) i czyści (używając truncate lub delete) tabele przed wywołaniem loadera (domyślnie PersisterLoader)


Jak powiedziałem wcześniej, ważne jest posiadanie dedykowanych fixture’ów dla każdego testu, dlatego chcemy, aby baza danych była pusta przed załadowaniem jakiegokolwiek fixture’a. Dlaczego więc nie skorzystać z PurgerLoadera? Ponieważ Doctrine tak tym się zajmuje:

<?php
$schemaTool = new SchemaTool($manager);
$schemaTool->dropDatabase();
$schemaTool->createSchema($manager->getMetadataFactory()->getAllMetadata());


Przypomnienie: każdy kontekst behat zostanie ponownie zbudowany (poprzez wywołanie funkcji constructor) w każdym scenariuszu.

Małe ostrzeżenie w tym momencie: SchemaTool jest NAPRAWDĘ niebezpieczną klasą, tak jak każda komenda bin/console doctrine:schema. Nie trzeba mówić, że $schemaTool->dropDatabase() może spowodować katastrofę, jeśli zostanie użyty w niewłaściwej bazie danych. Upewnij się, że nie możesz tego wywołać i nie używaj użytkownika, który ma uprawnienia do czyszczenia bazy danych na serwerze produkcyjnym. Oto przykład zapobiegania:

<?php
/** @var Connection[] $connections */
$connections = $doctrine->getConnections();
foreach ($connections as $connection) {
    if ('pdo_sqlite' !== $connection->getDriver()->getName()) {
        throw new \RuntimeException('Meaningful message here');
    }
}


Behat jest już gotowy do załadowania niektórych fixture’ów, ale potrzebuje czegoś do przetestowania.

Testowanie kontekstu

Kontekstem jest hipotetyczna aplikacja: API REST zarządzające produktami. Na produkty składają się z nazwy, ceny i są częścią ProductCategory, złożonej wyłącznie z nazwy. Oto encji Doctrine:

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
 */
class Product
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     *
     * @var int
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=255)
     *
     * @var string
     */
    private $name;
    /**
     * @ORM\Column(type="decimal", precision=10, scale=0)
     *
     * @var float
     */
    private $price;
    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\ProductCategory")
     * @ORM\JoinColumn(nullable=false)
     *
     * @var ProductCategory
     */
    private $category;
    public function getId(): int
    {
        return $this->id;
    }
    public function getName(): string
    {
        return $this->name;
    }
    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }
    public function getPrice(): float
    {
        return $this->price;
    }
    public function setPrice($price): self
    {
        $this->price = $price;
        return $this;
    }
    public function getCategory(): ProductCategory
    {
        return $this->category;
    }
    public function setCategory(ProductCategory $category): self
    {
        $this->category = $category;
        return $this;
    }
}

 

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity(repositoryClass="App\Repository\ProductCategoryRepository")
 */
class ProductCategory
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;
    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;
    public function getId(): ?int
    {
        return $this->id;
    }
    public function getName(): ?string
    {
        return $this->name;
    }
    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }
}


Oto kod akcji listującej produkty. Można je filtrować, przekazując argument „category” z nazwą kategorii w ciągu zapytania.

<?php 
namespace App\Action;
// use statements omitted for concision
class ListProductsAction
{
    // Properties & constructor omitted for concision
    
    public function __invoke(Request $request): JsonResponse
    {
        $filter = [];
        $categoryName = $request->query->get('category');
        if (null !== $categoryName) {
            $category = $this->productCategoryRepository->findOneBy(['name' => $categoryName]);
            $filter = ['category' => $category];
        }
        $products = $this->productRepository->findBy($filter);
        $response = [
            'type' => 'list:Product',
            'items' => $this->normalizer->normalize($products, 'json'),
            'total' => \count($products),
        ];
        return new JsonResponse($response);
    }
}

 

Pierwsze scenariusze w behat

By przetestować tę akcję potrzebne są dwa scenariusze:

  • Bez filtra (bez if)
  • Z filtrem według kategorii (wprowadzając if).


Aby to zrobić, musimy utworzyć 2 kategorie: „tools” i „lightning” oraz stworzyć różną liczbę produktów - powiedzmy 10 narzędzi (tools) i 5 produktów oświetleniowych (lightning). Pierwszy scenariusz powinien więc zawierać wszystkie 15 produktów, a następnie, filtrując według kategorii narzędzi, powinien mieć tylko 10 produktów.

Utwórz list-products.yaml na ścieżce fixture’ów:

App\Entity\ProductCategory:
category_tools:
name: 'tools'

category_lighting:
name: 'lighting'

App\Entity\Product:
tools_products_{1..10}:
name: '<firstName()>'
category: '@category_tools' # reference to "category_tools" fixture
price: '<randomFloat(2, 10.00, 1000.00)>' # random price from 10 to 1000 euros

lighting_products_{1..5}:
name: '<firstName()>'
category: '@category_lighting' # reference to "category_lighting" fixture
price: '<randomFloat(2, 10.00, 1000.00)>' # random price from 10 to 1000 euros


Oto, co można powiedzieć na temat tego pliku:

Najpierw węzły główne App\Entity\ProductCategory i App\Entity\Product: jest to FQCN tworzonych obiektów. W tym przypadku są to encje Doctrine, ale mogą być DOWOLNĄ klasą. Ten węzeł musi być unikalny w pliku, co oznacza, że nie jest możliwe napisanie jakiegoś Product fixture’a, a następnie ProductCategory, a jeszcze następnie samego produktu, jednak to nie jest problem, ponieważ kolejność fixture’ów nie ma znaczenia.

Następny poziom w drzewie to referencje category_tools, category_lighting, category_products_{1..10} i lightning_products_{1..5}. Referencje są naprawdę przydatne do wstrzykiwania fixture’ów do innych fixture’ów lub do uzyskania wygenerowanego fixture’a w celu uzyskania dostępu do jego właściwości (więcej o tym później).

Notacja {1..10} dla referencji produktu nazywa się to zakresem (patrz: dokument). Dla tools_products_{1..10} stworzy dziesięć fixture’ów, z do których można się odwołać przez referencję tools_products_1, tools_products_2, tools_products_3… i tak dalej.

Ostatni poziom drzewa to właściwości fixture’ów. W przypadku ProductCategory to nic szczególnego. Po prostu ustawia właściwość name za pomocą stałego ciągu znaków.

W przypadku produktu, właściwości name i price używają formatów Fakera: firstName i randomFloat. Dostępnych jest mnóstwo formatów, pełna lista znajduje się w dokumentacji. W razie potrzeby można również tworzyć własne formaty. Na koniec właściwość category wykorzystuje referencję do fixture’a. Referencje są poprzedzone przez @

Pamiętaj, że kolejność, w jakiej zapisywane są fixture’y, nie ma znaczenia: w tym przypadku fixture’y, do których istnieją odniesienia (ProductCategory), są zapisywane przed fixture’em, który się do nich odwołuje, ale i tak działa.

Teraz mamy wszystko, czego potrzebujemy do napisania scenariuszy behat:


@product @list-products
Feature:
In order to select products
As a customer
I want to see the products available

Background:
Given the fixtures file "list-products.yaml" is loaded

Scenario: List all products
When I send a "GET" request to "/products/"
Then the response status code should be 200
And the response should be in JSON
And the JSON node "total" should be equal to the number 15

Scenario: List products of a given category
When I send a "GET" request to "/products/" with parameters:
| key | value |
| category | tools |
Then the response status code should be 200
And the response should be in JSON
And the JSON node "total" should be equal to the number 10


W tych scenariuszach wykorzystano kroki z pakietu Behatch/contexts, który udostępnia niektóre typowe testy Behat.

Odpal Behat i voilà 🎉


Dodanie produktu - pobierz referencje do fixture’a i udostępnij je

Chcemy teraz móc dodawać produkty za pośrednictwem aplikacji. Produkty wymagają kategorii podczas ich tworzenia. Klient aplikacji będzie musiał podać id kategorii w danych:

{
"name": "name of the product",
"price": 10.00,
"category: <category_id>
}


Zaczniemy od scenariusza Behat i plików fixture’a, aby zastosować podejście TDD.

Rozpocznij od utworzenia pustej akcji:

<?php
declare(strict_types=1);
namespace App\Action;
use Symfony\Component\HttpFoundation\Request;
class AddProductAction
{
    public function __invoke(Request $request)
    {
        throw new \Exception('Not implemented yet');
    }
}


Aby sprawdzić, czy właściwie dodaliśmy produkt, musimy sprawdzić liczbę dostępnych produktów przed dodaniem nowego, a następnie dodać nowy. Na koniec sprawdzić całkowitą liczbę produktów. Wymaga to również istnienia co najmniej jednej kategorii.

Plik fixture’a i scenariusz Behat wyglądają następująco:

@product @add-products
Feature:
In order to offer a large choice of products
As a product seller
I want to add products

Background:
Given the fixtures file "add-products.yaml" is loaded

Scenario: Add a product
Given I send a "GET" request to "/products/"
And the response status code should be 200
And the JSON node "total" should be equal to the number 5
When I send a "POST" request to "/products/" with body:
"""
{
"name": "Super cool drill",
"price": 249.99,
"category": 1
}
"""
Then the response status code should be 200
And I send a "GET" request to "/products/"
And the JSON node "total" should be equal to the number 6

 

App\Entity\ProductCategory:
category_tools:
name: 'tools'

App\Entity\Product:
tools_products_{1..5}:
name: '<firstName()>'
category: '@category_tools' # reference to "category_tools" fixture
price: '<randomFloat(2, 10.00, 1000.00)>' # random price from 10 to 1000 euros

 

Ważną rzeczą, na którą należy tutaj zwrócić uwagę, jest identyfikator kategorii, który przekazujemy w danych żądania POST: “category”: 1. Ten test zakłada, że identyfikator kategorii “tools” będzie wynosił 1, ale nic tego nie gwarantuje. 

Po pierwsze, możesz używać UUID na produkcji (btw, to dobry pomysł). Po drugie, wyobraź sobie, że ten test powiększa się, ponieważ dodajemy funkcje i mamy teraz kilka kategorii. Jeśli testy polegają na identyfikatorach generowanych przez bazę danych (automatycznie inkrementowanych) i musisz dodać kategorię między dwiema istniejącymi kategoriami, musisz edytować wszystkie testy.

Wprowadzenie SharingContext

Aby rozwiązać ten problem, musimy dynamicznie zmieniać dane żądania POST, aby użyć rzeczywistego identyfikatora kategorii „tools”. Sama kategoria może zostać pobrana poprzez referencję @category_tools. Dążymy do możliwości zrobienia czegoś takiego:

Given I store the "id" property of "category_tools" as "category_tools_id"
When I send a "POST" request to "/products/" with body:
"""
{
"name": "Super cool drill",
"price": 249.99,
"category": category_tools_id
}
"""


Problem polega na tym, jak przekazać wartość „category_tools_id” między 2 krokami. Na szczęście Behat zapewnia sposób na jego rozwiązanie. Zobacz: Dostęp do danych innego kontekstu. Stworzymy SharingContext, który będzie odpowiedzialny za przechowywanie wartości, które muszą być współdzielone między różnymi kontekstami.

Dzięki szablonowi twig (podziękowania dla Alexandre Salomé za ten pomysł) jeszcze łatwiej będzie uzyskać dostęp i dzielić właściwości fixture’a w następujący sposób:

When I send a "POST" request to "/products/" with body:
"""
{
"name": "Super cool drill",
"price": 249.99,
"category": {{ category_tools.id }}
}
"""


Stworzymy również funkcję SharedContextTrait, aby Contexts miały łatwy dostęp do SharingContext.

<?php
declare(strict_types=1);
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
trait SharedContextTrait
{
    /** @var SharingContext */
    private $sharingContext;
    /**
     * @BeforeScenario
     */
    public function gatherSharingContext(BeforeScenarioScope $scope): void
    {
        /** @var InitializedContextEnvironment $environment */
        $environment = $scope->getEnvironment();
        $this->sharingContext = $environment->getContext(SharingContext::class);
    }
}

 

<?php
declare(strict_types=1);
use Behat\Behat\Context\Context;
use Behat\Symfony2Extension\Context\KernelAwareContext;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Twig\Environment;
class SharingContext implements Context, \ArrayAccess, KernelAwareContext
{
    /** @var array */
    private $values = [];
    /** @var Environment */
    private $twig;
    public function setKernel(KernelInterface $kernel): void
    {
        /** @var ContainerInterface $container */
        $container = $kernel->getContainer();
        if (!$container->has('twig')) {
            throw new \LogicException(
                "Could not find 'twig' service. Try running 'composer req --dev twig/twig symfony/twig-bundle'."
            );
        }
        /** @var Environment $twig */
        $twig = $container->get('twig');
        $this->twig = $twig;
    }
    public function renderTwigTemplate(string &$string): void
    {
        $template = $this->twig->createTemplate($string);
        $string = $this->twig->render($template, $this->values);
    }
    public function offsetExists($offset): bool
    {
        return array_key_exists($offset, $this->values);
    }
    public function offsetGet($offset)
    {
        return $this->values[$offset];
    }
    public function offsetSet($offset, $value): void
    {
        $this->values[$offset] = $value;
    }
    public function offsetUnset($offset): void
    {
        unset($this->values[$offset]);
    }
    public function merge(array $array): void
    {
        $this->values = array_merge($this->values, $array);
    }
}


SharingContext implementuje interfejs ArrayAccess, dzięki czemu możemy pobierać i ustawiać wartości jak za pomocą tablicy:

<?php
$this->sharingContext['key'] = $value; // set a value
$value = $this->sharingContext['key']; // retrieve value


Musimy nadpisać krok When I send a "POST" request to "/products/" with body:, aby użyć szablonu twig z SharingContext.

<?php
declare(strict_types=1);
use Behat\Gherkin\Node\PyStringNode;
use Behatch\Context\RestContext as BehatchRestContext;
class RestContext extends BehatchRestContext
{
    use SharedContextTrait;
    /**
     * @override Given I send a :method request to :url with body:
     */
    public function iSendARequestToWithBody($method, $url, PyStringNode $body)
    {
        $rawBody = $body->getRaw();
        $this->sharingContext->renderTwigTemplate($rawBody);
        $newBody = new PyStringNode(explode("\n", $rawBody), $body->getLine());
        return parent::iSendARequestToWithBody($method, $url, $newBody);
    }
}


Jeśli zrzucimy treść żądania w Action i uruchomimy Behat (nie zapomnij dodać nowo utworzonych kontekstów do behat.yaml), zauważymy, że {{ category_tools.id }} został zastąpiony przez 1:

Teraz, gdy scenariusz Behat jest gotowy, napisanie funkcji jest bardzo łatwe i wygodne, ponieważ nie trzeba ręcznie wysyłać żądania POST do API, a jedynie uruchamiać scenariusz behat, dopóki nie zakończy się sukcesem.

<?php
/** .... */
class AddProductAction
{
    /** .... */
    public function __invoke(Request $request)
    {
        $payload = json_decode($request->getContent(), true);
        $category = $this->productCategoryRepository->find($payload['category']);
        
        $product = (new Product())
            ->setName($payload['name'])
            ->setPrice($payload['price'])
            ->setCategory($category);
        $this->entityManager->persist($product);
        $this->entityManager->flush();
        return new Response('Product created');
    }
}

 


Podsumowanie

Fixture’y świetnie nadają się do łatwego tworzenia początkowego stanu testów i testowania integracji bazy danych.

Dzięki Behat, SharingContext i szablonom twig, bardzo łatwo jest pisać czytelne scenariusze, a dynamiczne części bazy danych już nie stanowią problemu.

Do tego przykładu użyłem RestContext, aby użyć fixture’ów w danych zapytań API (mogłem również użyć ich w adresach URL), ale jest o wiele więcej takich zastosowań. Osobiście mam ProcessorContext, z którym testuję procesory swarrot, przekazując im treść wiadomości z danymi fixture’ów, a następnie używam DatabaseContext, aby sprawdzić, jak wpisy zostały zmodyfikowane przez procesor.

Alice jest naprawdę potężną biblioteką (sprawdź dokumentację). Jest prawie niemożliwe, żeby nie pasowała do Twoich potrzeb.

Jeśli chcesz z tym poeksperymentować, demo kod znajdziesz na Githubie.


Oryginał tekstu w języku angielskim przeczytasz tutaj.

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 120 tysiącami naszych czytelników

Dowiedz się więcej