Как тестировать приватные сервисы в Symfony.

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. Симпатично, не правда ли?

Это перевод этой статьи

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