Как переиспользовать код с бандлами Symfony 5? Часть 6. Тестирование

от автора

Поговорим о том, как прекратить копипастить между проектами и вынести код в переиспользуемый подключаемый бандл Symfony 5. Серия статей, обобщающих мой опыт работы с бандлами, проведет на практике от создания минимального бандла и рефакторинга демо-приложения, до тестов и релизного цикла бандла.

В этой статье разберем как тестировать бандл, напишем несколько тестов и создадим внутри бандла микроприложение для их запуска.

Если вы не последовательно выполняете туториал, то скачайте приложение из репозитория и переключитесь на ветку 5-configuration.

Инструкции по установке и запуску проекта в файле README.md. Финальную версию кода для этой статьи вы найдете в ветке 6-testing.

Перенос тестов в бандл

До рефакторинга уже было написано 2 теста: юнит тест одного из сервисов и функциональный тест контроллера.

У нас примитивное приложение, в которой слой инфраструктуры (БД) не отделен от доменной логики: тесты используют временную базу данных sqlite.

Проверим, работают ли тесты:

./vendor/bin/simple-phpunit

Error: Class 'App\Service\EventExporter\Exporters\GoogleCalendarExporter' not found

В тестах остались названия пространств имен App\, оставшиеся от кода до его переноса в бандл.

Исправим в обоих тестах пространства имен на bravik\CalendarBundle и запустим снова.
Тесты должны пройти успешно.

В корне бандла создадим папку tests (bundles/CalendarBundle/tests) и перенесем туда папку tests/Service с юнит тестом. Функциональный тест в папке Controller перенесем чуть позже.

Запуск юнит-тестов из бандла

Чтобы запустить тест, нам потребуется установить PHPUnit внутри бандла:

cd bundles/CalendarBundle composer require symfony/phpunit-bridge --dev

Это добавит PHP Unit в качестве dev-зависимости в composer.json бандла, а так же создаст файл composer.lock. В бандлах он нам не нужен: создайте .gitignore файл и добавьте его туда.

PHP Unit «из коробки» не заработает. Его нужно настроить: указать где лежат тесты и подключить автозагрузчик composer.

Скопируйте из приложения-хоста в бандл файл phpunit.xml.dist.

Благодаря тому, что структура папок бандла идентична обычному Symfony-приложению, поменять нужно всего 1 строку bootstrap:

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:noNamespaceSchemaLocation="bin/.phpunit/phpunit.xsd"          backupGlobals="false"          colors="true"          bootstrap="./vendor/autoload.php" >

Здесь в аттрибуте bootstrap нам нужно указать путь к автолоадеру composer, чтобы PHPUnit мог воспользоваться стандартным механизмом автозагрузки классов.

Запускаем тест в бандле:

./vendor/bin/simple-phpunit

Тест должен успешно пройти!

Теперь попробуем протестировать контроллер: скопируйте тест из приложения-хоста в бандл.

И… как теперь его запускать?
Ведь в бандле нет приложения.

Создание микроприложения внутри бандла

В первой статье про бандлы, мы описали 2 способа разработки бандла.
Временно, — для удобства, — мы разрабатывали бандл прямо внутри приложения-хоста.
Но пора оторваться от родительского дома и пустить наш бандл во взрослую жизнь.

Чтобы хранить и запускать тесты бандла из самого бандла, и дальше его автономно разрабатывать,
создадим внутри него минималистичное микроприложение-хост.

Главный компонент веб-приложения Symfony — его ядро Kernel из пакета symfony/http-kernel.
Ядро принимает запрос, передает его обработчику, и возвращает ответ.

Можно подключить этот пакет отдельно. Но, с другой стороны, сложно представить себе приложение Symfony без DI-контейнера, роутинга и прочих плюшек. Поэтому сразу подключим набор пакетов symfony/framework-bundle, в который входит и http-kernel.

composer require symfony/framework-bundle 

Создадим папку tests/App и класс TestingKernel внутри.

Новый класс унаследуем от Symfony\Component\HttpKernel\Kernel и реализуем два абстрактных метода, которые требует от нас родитель:

<?php namespace bravik\CalendarBundle\Tests\App;  use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\HttpKernel\Kernel;  class TestingKernel extends Kernel {     public function __construct()     {         parent::__construct('test', false);     }      public function registerBundles()     {         // TODO: Implement registerBundles() method.     }      public function registerContainerConfiguration(LoaderInterface $loader)     {         // TODO: Implement registerContainerConfiguration() method.     } }

Чтобы пользоваться автозагрузчиком composer мы указали для ядра пространство имен bravik\CalendarBundle\Tests\App.

На вход конструктора ядро принимает обязательные параметры: строковую константу 'test', обозначающую окружение и опцию включения отладочного режима. Здесь мы переопределяем конструктор, чтобы сразу зафиксировать нужные нам значения.

Метод registerBundles() возвращает массив с инстанциированными классами всех подключенных бандлов. Подключим наш:

public function registerBundles() {     return [       new CalendarBundle()      ]; }

Метод registerContainerConfiguration() загружает конфиги и формирует DI-контейнер.
Пока оставим его пустым.

Чтобы протестировать контроллер нам потребуется компонент symfony/router. С его помощью, микроприложение должно научиться считывать аннотации в контроллерах и сопоставлять роуты экшнам.

Давайте посмотрим как это делается в обычном приложении. Загляните в src/Kernel приложения-хоста:

class Kernel extends BaseKernel {     use MicroKernelTrait;      //...      protected function configureRoutes(RouteCollectionBuilder $routes): void     {         //...         $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');     } }

Мы видим, что обычное Symfony-приложение использует «допинг» в виде трейта MicroKernelTrait.

Внутри него уже реализован метод registerContainerConfiguration(), конфигурирующий контейнер
и добавлено два абстрактных метода-хука:

  • configureContainer() для дальнейшей настройки контейнера
  • configureRoutes(), где можно определить или загрузить роуты нашего приложения.

Сделаем аналогично:

  • уберем в нашем TestingKernel метод registerContainerConfiguration(),
  • добавим use MicroKernelTrait;
  • сгенерируем недостающие реализации его абстрактных методов.

Внутри метода configureRoutes() загрузим конфигурацию аннотаций из файла config/routes.yaml бандла:

protected function configureRoutes(RouteCollectionBuilder $routes) {     $routes->import(__DIR__.'/../../config/routes.yaml'); }

Так как наш конфиг в формате yaml, нам потребуется добавить компонент для его парсинга:

composer require symfony/yaml

Теперь мы имеем минимальное ядро для запуска приложения.

Но как использовать его в тестах?

Работа с функциональными тестами из бандла

Вернемся в тест tests/Controller/EventControllerTest.

Чтобы протестировать контроллер нам нужно отправить к нему HTTP запрос. По идее здесь нам потребуется браузер или другой HTTP-клиент. Однако фреймворк устроен таким образом, что настоящий браузер использовать не обязательно.

В Symfony запросы браузера моделируются абстракцией Request, а потом передаются в ядро для обработки. Посмотрим на index.php:

$request = Request::createFromGlobals(); $response = $kernel->handle($request);

Аналогично этому мы можем сформировать фейковый запрос вручную, отдать его ядру и проверить возвращаемый им результат.

Для упрощения работы с такими запросами, в Symfony есть класс HttpKernelBrowser и специальный пакет:

composer require symfony/browser-kit --dev

В тесте это может выглядеть так:

public static function createClient() {     $kernel = new TestingKernel();     return new HttpKernelBrowser($kernel); }  public function testSomeAction() {     $client = static::createClient();     $response = $client->request("/some/action");     // Assertion on response     // ... }

Здесь мы инициализируем ядро и передаем его в HttpKernelBrowser. После этого имитируем запросы к ядру через $client->request() и тестируем полученный результат.

Вместо того, чтобы инициализировать ядро и клиент вручную, мы унаследуем наш тест от класса WebTestCase, предоставленному Symfony. В этом классе уже определен метод createClient(), создающий клиента, и метод createKernel(), создающий ядро.

Единственное, что нам потребуется сделать, — это указать какое именно ядро нужно использовать. Это делается установкой переменной окружения KERNEL_CLASS в phpunit.xml.dist:

<php>     <server name="APP_ENV" value="test" force="true" />     <server name="KERNEL_CLASS" value="bravik\CalendarBundle\Tests\App\TestingKernel"             force="true" />     <!-- ... --> </php>

Теперь можно попытаться запустить наш тест:

./vendor/bin/simple-phpunit tests/Controller/EventControllerTest.php

Упс…

LogicException: Container extension "framework" is not registered

Ошибка пришла к нам из MicroKernelTrait. Этот класс добавляет в DI-контейнер немного конфигурации «по-умолчанию», в том числе для компонента framework.

Но мы еще не добавили в ядро FrameworkBundle. Сделаем это:

    public function registerBundles()     {         return [             new FrameworkBundle(),             new CalendarBundle()         ];     }

Запустим тест еще раз:

InvalidArgumentException: Cannot determine controller argument for "bravik\CalendarBundle\Controller\EditorController::new()": the $entityManager argument is type-hinted with the non-existent class or interface: "Doctrine\ORM\EntityManagerInterface".

Что-тут происходит? При чем тут EditorController?

Когда мы подключили new CalendarBundle() в TestingKernel, бандл подключил к сборке свой конфиг services.yaml, в котором у нас явно определены необходимые сервисы и их зависимости.

Кроме этого мы пользуемся механизмом autowiring Symfony внутри самих этих сервисов. Мы указываем с помощью typehints нужные нам сервисы в конструкторе или экшнах контроллера, а Symfony при компиляции контейнера считывает эти typehints и автоматически добавляет их в конфигурацию зависимостей.

Но чтобы они там оказались, нужно установить, сконфигурировать и загрузить все используемые нашим бандлом зависимости в ядро TestingKernel.

Установим используемые в бандле зависимости:

composer require doctrine/orm doctrine/doctrine-bundle symfony/twig-bundle composer require doctrine/doctrine-fixtures-bundle liip/test-fixtures-bundle --dev

Подключим к ядру TestingKernel:

public function registerBundles() {     return [         new DoctrineBundle(),         new DoctrineFixturesBundle(),         new LiipTestFixturesBundle(),         new TwigBundle(),         //..     ]; }

Создадим конфигурационный файл: tests/App/config/config.yaml:

# Обязательный параметр для тестирования # @see https://symfony.com/doc/current/reference/configuration/framework.html#test framework:   test:   true  doctrine:   # Подключаем SQLITE БД для тестов в var/test.db   dbal:     driver: pdo_sqlite     path: "%kernel.cache_dir%/test.db"    # Подключаем ORM-мэппинг сущностей   orm:     auto_generate_proxy_classes: true     naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware     auto_mapping: true  calendar:   enable_soft_delete: true  services:   # Расширение Twig, предоставляющее mock-функции вместо функций webpack encore,   # которые используются в шаблонах   bravik\CalendarBundle\Tests\App\TwigWebpackSuppressor:     tags: ['twig.extension']    # Фикстуры для тестов должны быть помечены тегом   bravik\CalendarBundle\Tests\Fixtures\:     resource: '../../Fixtures'     tags: ['doctrine.fixture.orm']

В отличие от обычного приложения, где конфигурация разбивается по бандлам в папке packages, мы поместим все в общий конфигурационный файл. Здесь нам нужно определить обязательные параметры для Framework, Doctrine, нашего собственного бандла и зарегистрировать фикстуры.

После этого подключим конфиг к ядру TestingKernel:

protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) {     $loader->load(__DIR__.'/config/config.yaml', 'yaml'); }

(!) Обратите внимание, что теперь, при запуске теста в корне банла создастся папка var с кэшэм и логами как в обычном Symfony приложении. Её нужно добавить в .gitignore.

Поскольку микроприложение будет в режиме тестирования, то кэш не будет очищаться при изменениях кода и конфигурации в отличии от dev режима. Поэтому в случае возникновения каких-то проблем, прежде всего — очистите кэш вручную!

Запускаем тест снова, и наконец-то успех!

Мы запустили наше микроприложение, контейнер скомпилировался, роутинг подключился и тест достучался до нашего контроллера.

Резюме

Мы разобрались как писать и запускать тесты непосредственно в бандле, а так же погрузились в устройство Symfony и создали микро-приложения для запуска тестов прямо внутри бандла.

Финальную версию кода для этой статьи вы найдете в ветке 6-testing.

В следующей статье поговорим о том как выпускать новые релизы бандла без боли и страданий, как организовать процесс первичной установки и обновления бандла.

Другие статьи серии:

Часть 1. Минимальный бандл
Часть 2. Выносим код и шаблоны в бандл
Часть 3. Интеграция бандла с хостом: шаблоны, стили, JS
Часть 4. Интерфейс для расширения бандла
Часть 5. Параметры и конфигурация
Часть 6. Тестирование, микроприложение внутри бандла
Часть 7. Релизный цикл, установка и обновление

ссылка на оригинал статьи https://habr.com/ru/post/500044/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *