2 версии Symfony подвержены рассогласованностью между сервисами и тестами. Вы используете Symfony 3.4 или 4.0? Вы хотите тестировать сервисы, но не знаете как получить их правильно? Сегодня мы посмотрим на возможные решения.
Если вы знаете проблему и ищите только решение, вы можете перейти сразу к решению для Symfony 4.1 или Symfony 3.4/4.0
Начиная с Symfony 3.4 все сервисы приватны по умолчанию. Это значит, что вы не можете получить сервис по средствам $this->get(App\SomeService::class) или $this->container->get(App\SomeService::class) , but но только через конструктор.
Это хорошо, пока вы не захотите протестировать сервис
use App\SomeService; use PHPUnit\Framework\TestCase; final class SomeServiceTest extends TestCase { public functoin testSomeMethod() { $kernel = new AppKernel; $kernel->boot(); $container = $kernel->getContainer(); // эта строка важна ↓ $someService = $container->get(SomeService::class); // ... } }
Когда мы запускаем тесты:
vendor/bin/phpunit tests
Это исключение остановит нас:
The "App\SomeService" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead.
…Сделать его публичным…
Хорошо:
# app/config/config.yml services: _defaults: autowire: true App\: resource: .. + + App\SomeService: + public: true
И запустим тесты еще раз:
vendor/bin/phpunit tests
Работает!
Спускаемся в пахнущую кроличью нору
Как вы можете видеть, мы можем загружать десятки сервисов из App\ с помощью двух строк. Но что бы тестировать 1 нам нужно добавить 2 дополнительные строки в конфигурацию.
# app/config/config.yml services: _defaults: autowire: true App\: resource: .. + + # for tests only + App\SomeService: + public: true + + App\AnotherService: + public: true + + App\YetAnotherService: + public: true
Мы также можем извлечь это в тестовую конфигурацию tests/config/config.yml
, это просто, что бы скрыть «запах»
Или просто сделать все сервисы публичными:
services: _defaults: autowire: true + # for tests only + public: true App\: resource: ..
это быстрое и простое решение, но оно не подходит для длительных и или больших проектов
Не беспокойтесь, вы не одиноки. Тут больше 36 результатов для запроса «symfony tests private services» на StackOverflow:
Что мы можем сделать?
1. В Symfony 4.1 с FrameworkBundle
Сейчас это исправлено в Symfony 4.1 with Simpler service testing.
Вы используете:
Symfony\Bundle\FrameworkBundle\Test\KernelTestCase или Symfony\Bundle\FrameworkBundle\Test\WebTestCase
просто обновите Symfony до версии 4.1.
2. В Symfony 4.1 без FrameworkBundle или Symfony 3.4/4.0
Но если вы не используете FrameworkBundle, то есть разрабатываете пакет и используете Symfony\Console и Symfony\DependencyInjection?
В этом случае, разумно оставить всю конфигурацию как есть, не важно в каком мы окружении: в dev или test.
# app/config/config.yml services: _defaults: autowire: true App\: resource: ..
И тесты соответственно
use App\SomeService; use PHPUnit\Framework\TestCase; final class SomeServiceTest extends TestCase { public functoin testSomeMethod() { $kernel = new AppKernel; $kernel->boot(); $container = $kernel->getContainer(); $someService = $container->get(SomeService::class); // ... } }
Было бы здорово, если было бы одно место с условием, которое сделает весь код чистым и позволит нам тестировать сервисы. Это круто, верно? Как мы можем этого достигнуть? Идеи?
«Как насчет Compiler Pass?»
Конечно, Compiler Pass! Одна из самых лучших особенностей в Symfony — это возможности заложенные в архитектуре. Это позволяет вам писать чудесный, независимый и повторно используемый код по-умолчанию. После всего этого решение для Symfony 4.1 было закончено с Compiler Pass, that creates public ‘test.service-name’ aliases.
Вы можете писать ваши собственные обертки которые закрывают потребности вашего приложения (рекомендуется) или, если вы используете PHPUnit, использовать Symplify\PackageBuilder:
use Symfony\Component\HttpKernel\Kernel; +use Symplify\PackageBuilder\DependencyInjection\CompilerPass\PublicForTestsCompilerPass; final class AppKernel extends Kernel { protected function build(ContainerBuilder $containerBuilder): void { $containerBuilder->addCompilerPass('...'); + $containerBuilder->addCompilerPass(new PublicForTestsCompilerPass()); } }
Это позволяет удалить все public:true из кода.
Где магия?
Вся эта «магия» в PublicForTestsCompilerPass
:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; final class PublicForTestsCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $containerBuilder): void { if (! $this->isPHPUnit()) { return; } foreach ($containerBuilder->getDefinitions() as $definition) { $definition->setPublic(true); } foreach ($containerBuilder->getAliases() as $definition) { $definition->setPublic(true); } } private function isPHPUnit(): bool { // defined by PHPUnit return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__'); } }
Этот код делает все сервисы публичными, когда запускается PHPUnit. Симпатично, не правда ли?
Это перевод этой статьи