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