Как переиспользовать код с бандлами 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. Релизный цикл, установка и обновление

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 5

      0
      А что делать, если мой бандловый контроллер использует сервисы и репозитории из «родительского» приложения? Я экстрактировал интерфейсы из этих сервисов и репозиториев и сложил их в отдельный Composer пакет, который в свою очередь рекуайрится и в «родительское» приложение, и в бандл. Соответственно, бандл программируется к этим интерфейсам. И в составе всего приложения бандл работает прекрасно. А как теперь протестировать это в изолированном состоянии в бандле? Ибо при запуске тестов получаем:
      Symfony\Component\DependencyInjection\Exception\RuntimeException: Cannot autowire service «bundle_controller_service_id»: argument "$eventStore" of method «My\Bundle\Namespace\CallbackController::__construct()» references interface «Vendor\Contracts\BundleEventStoreInterface» but no such service exists. Did you create a class that implements this interface?
        0

        Можно в приложении для тестов сделать mock-реализации интерфейсов (ничего не делающие по факту или отдающие "болваночные" данные, но реализующие контракт интерфейса) от которых зависит ваш бандл.

          0
          Но в таком случае, как я понимаю, нужно 1) отключить autowiring в бандловом services.xml, 2) явно указать mock-реализации как argument для моего бандлового сервиса, 3) добавить интерфейсы как alias'ы для mock-реализаций. Но что в таком случае произойдет после компиляции контейнера, когда бандл будет установлен в приложение и подгрузится бандловый же services.xml? Алиасы для интерфейсов будут же указывать на mock-реализации! Или для тестов нужен какой-то отдельный services.xml?
            0

            Тут в статье расписан же процесс: вы для запуска тестов делаете прямо в бандле в папке tests отдельное микроприложение из нескольких файлов, со своим контейнером и его конфигурацией. В его контейнере вы можете вашему интерфейсу назначить конкретную мок-реализацию, аналогично тому как вы назначаете реализацию в обычном приложении. Интерфейсы же для этого и созданы: чтобы вы с легкостью могли его подменить для нужного кейса нужной реализацией.

              0
              Да, сорри, чот затупил. Спасибо!

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

      Самое читаемое