28 нояб. 2021 г.
Всем привет. Сегодня хотел бы поделиться способом организации фикстур в Symfony приложении с помощью Codeception Doctrine2 модуля.
Фикстуры используются для генерации тестовых данных. Чаще всего, для тестовые данные бывают нужны для интеграционных тестов. В Symfony фикстуры реализованы в рамках DoctrineFixturesBundle.
Фикстура может выглядеть примерно так:
// src/DataFixtures/AppFixtures.php
namespace App\DataFixtures;
use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
// create 20 products! Bam!
for ($i = 0; $i < 20; $i++) {
$product = new Product();
$product->setName('product '.$i);
$product->setPrice(mt_rand(10, 100));
$manager->persist($product);
}
$manager->flush();
}
}
Такой подход работает, однако, возможно несколько проблем.
В результате создание и поддержка фикстур может оказаться довольно утомительным занятием. Для более удобной организации фикстур можно использовать DataFactory из пакета Codeception. При этом необязательно использовать полностью весь фреймворк.
Для работы фикстур нужно установить DoctrineFixturesBundle.
composer require doctrine/doctrine-fixtures-bundle
Также нам понадобится модуль Doctrine2 для Codeception.
composer require --dev codeception/module-doctrine2
Для генерирования случайных данных может пригодиться Faker.
composer require fzaninotto/faker
Правда, в конце 2020 автор прекратил разработку пакета. Поэтому, возможно, стоит рассмотреть альтернативные варианты или форк. Или вовсе отказаться от случайных данных в фикстурах.
Cоздадим класс AbstractFunctionalTest и определим в нем логику взаимодействия с фикстурами. В дальнейшем от этого класса будут наследоваться все функциональные тесты.
Затем создадим класс DataFactory и методы для создания кастомных сущностей. Для создания каждой сущности создается отдельный метод. Сначала создается массив значений по-умолчанию, затем этот массив объединяется с массивом параметров. Таким образом, в фикстуре можно указать только необходимые для конкретного теста значения. Также по-умолчанию создаются связанные сущности.
<?php
declare(strict_types=1);
namespace App\Tests\Factory;
use App\Entity\Environment;
use App\Entity\Feature;
use App\Entity\FeatureValue;
use App\Entity\Project;
use Codeception\Lib\Di;
use Codeception\Lib\ModuleContainer;
use Codeception\Module\Doctrine2;
use Doctrine\ORM\EntityManager;
use Faker\Factory;
use Faker\Generator;
class DataFactory
{
protected EntityManager $entityManager;
private Doctrine2 $doctrine;
protected Generator $faker;
public function __construct(
EntityManager $entityManager
) {
$this->entityManager = $entityManager;
$this->faker = Factory::create();
$this->initializeDoctrineModule();
}
public function createProject(array $data = []): Project
{
$data = $this->setDefaultValues($data, [
'name' => 'demo',
'description' => 'demo project',
'owner' => 'antonshell',
'readKey' => bin2hex(random_bytes(64)),
'manageKey' => bin2hex(random_bytes(64)),
]);
$data['readKey'] = password_hash($data['readKey'], PASSWORD_BCRYPT);
$data['manageKey'] = password_hash($data['manageKey'], PASSWORD_BCRYPT);
return $this->doctrine->grabEntityFromRepository(Project::class, [
'id' => $this->doctrine->haveInRepository(Project::class, $data),
]);
}
public function createEnvironment(array $data = []): Environment
{
$data = $this->setDefaultValues($data, [
'name' => 'prod',
'description' => 'Production environment',
]);
if (!array_key_exists('project', $data)) {
$project = $this->createProject();
$data['project'] = $project;
$data['project_id'] = $project->getId();
}
return $this->doctrine->grabEntityFromRepository(Environment::class, [
'id' => $this->doctrine->haveInRepository(Environment::class, $data),
]);
}
protected function setDefaultValues(array $data, array $defaults): array
{
return array_merge($defaults, $data);
}
private function initializeDoctrineModule(): void
{
$di = new Di();
$moduleContainer = new ModuleContainer($di, []);
$this->doctrine = new Doctrine2($moduleContainer);
$reflectionProperty = new \ReflectionProperty(Doctrine2::class, 'em');
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($this->doctrine, $this->entityManager);
}
}
DataFactory можно использовать в фикстурах таким образом:
<?php
declare(strict_types=1);
namespace App\Tests\DataFixtures\Repository;
use App\Tests\Factory\DataFactory;
use App\Tests\DataFixtures\BaseFixture;
use Doctrine\Persistence\ObjectManager;
class SomeFixture extends BaseFixture
{
public function load(ObjectManager $objectManager): void
{
$factory = new DataFactory($objectManager);
$project = $factory->createProject([
'name' => 'Test project',
'owner' => 'antonshell',
'readKey' => 'read_key',
]);
$this->addReference('test_project_ref', $project);
}
}
Также, для избежания повторения кода в фикстурах, можно создавать конфигурацию фикстуры. Создается массив с определением параметров создаваемых сущностей. Само создание сущностей происходит в родительском классе. Пример реализации: FeatureControllerFixture, BaseFixture. Однако, в этом случае важно сохранять баланс и видеть границу между удобством и гибкостью.
Данный подход к организации фикстур использовал в нескольких проектах. В результате создание фикстур и написание функциональных тестов значительно упрощалось. Пример реализации и использования доступен в сервисе feature-флагов: https://github.com/antonshell/feature-flags.
На этом пока все. Спасибо за внимание!