How to Test Private Services in Symfony

2 versions of Symfony are affected by this dissonance between services and tests. Do you use Symfony 3.4 or 4.0? Do you want to test your services, but struggle to get them in a clean way?

Today we look at possible solutions.

If you know the problem and look only for a solution, jump right down to Symfony 4.1 Standalone or Symfony 3.4/4.0 solution.


Since Symfony 3.4 all services are private by default. That means you can't get service by $this->get(App\SomeService::class) or $this->container->get(App\SomeService::class) anymore, but only only via constructor.

That's ok until you need to test such service:

use App\SomeService;
use PHPUnit\Framework\TestCase;

final class SomeServiceTest extends TestCase
{
    public functoin testSomeMethod()
    {
        $kernel = new AppKernel;
        $kernel->boot();
        $container = $kernel->getContainer();

        // this line is important ↓
        $someService = $container->get(SomeService::class);
        // ...
    }
}

When we run the test:

vendor/bin/phpunit tests

This exception will stop us:

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.

...make it public...

Ok!

 # app/config/config.yml
 services:
     _defaults:
         autowire: true

     App\:
         resource: ..
+
+    App\SomeService:
+        public: true

And run tests again:

vendor/bin/phpunit tests

Voilá!

Down the Smelly Rabbit Hole

As you can see, we can load dozens of service from App\ by 2 lines. But to test 1, we need to add 2 extra lines to config.

 # 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

This is one to many code smell.

Also, we can extract it to test config tests/config/config.yml, so it's easier to hide the smell.

Or just make everything public, like I did in Symplify 6 months ago:

 services:
     _defaults:
         autowire: true
+        # for tests only
+        public: true

      App\:
          resource: ..

It's fast and easy solution, but...

Not a way to go in long-term or bigger projects.

Don't worry, you're not alone. There are over 36 results for "symfony tests private services" on StackOverflow at the time being:

But what other saint options we have?

1. In Symfony 4.1 with FrameworkBundle

This is now fixed in Symfony 4.1 with Simpler service testing.

Do you use

for your tests? Just upgrade to Symfony 4.1 and you're done.

2. In Symfony 4.1 Standalone or Symfony 3.4/4.0

But what if you don't use FrameworkBundle, e.g. you develop standalone packages and you only use Symfony\Console and Symfony\DependencyInjection?

It's reasonable we want to keep all configs untouched, no matter if we're in dev or tests.

# app/config/config.yml
services:
    _defaults:
        autowire: true

    App\:
        resource: ..

And tests as well:

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);
        // ...
    }
}

If there would only be one place with a switch, that would make that all code smells go away and let us test. That would be awesome, right? How can we achieve that? Any ideas?

"What about Compiler Pass?"

Exactly, a Compiler Pass! One of the best features in Symfony - by abilities and by architecture design. It leads you to write nice, decoupled and reusable code by default. After all, the solution for Symfony 4.1 is done by Compiler Pass, that creates public 'test.service-name' aliases.

You can write your own that covers needs of your own application (much recommended) or if you use PHPUnit, make use of the one in 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());
     }
 }

This removed all public: true code smells from Symplify, just see this beautiful commit:

Voilá!

Where is the Magic?

This is all the "magic" in 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__');
    }
}

It makes every services public, when in PHPUnit run. Pretty clear, huh?

There is also similar package by a friend of mine Tobias Nyholm called SymfonyTest/symfony-bundle-test. Check at least the super short CompilerPass.

Other Existing Solutions

I don't find them useful myself, but maybe you will.

Diversity Matters

Do you have another solution? Just drop a comment, all add it here.

I'm very curious to find all the solutions people make for 1 problem they have common. How these solutions are diverse, but also similar.



Happy Symfony service testing!


Typo? Fix it, please  and join 29 people who build this website

GitHub RSS @votrubaT Runs on Statie Hosted on GitHub Build by 30 people

Like what I write about? Hire me & we can work together