New in Symfony 4.1: Simpler service testing

Contributed by
Nicolas Grekas
in #26499.

In Symfony 3.4 we made all services private by default meaning that you cannot longer call $this->get('my_service_id') in your controllers to quickly get some service.

We made this change because using the service container directly is not considered a good practice: it hides the dependencies of your classes, making them coupled to external configuration, thus harder to test and to review.

Whenever we remove a feature like that, we provide an alternative that is considered better and, if possible, as simple to use as the previous one. That's why controllers allow injecting services with type hints in their action methods and their constructors.

The only remaining drawback of "private services by default" is that testing was harder than before. Some developers even defined some config in the test environment to make all services public in tests. In Symfony 4.1, we did the same and now tests allow fetching private services by default.

In practice, tests based on WebTestCase and KernelTestCase now access to a special container via the static::$container property that allows fetching non-removed private services:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class AddUserCommandTest extends WebTestCase
{
    private function assertUserCreated()
    {
        self::bootKernel();

        // returns the real and unchanged service container
        $container = self::$kernel->getContainer();

        // gets the special container that allows fetching private services
        $container = self::$container;

        $user = self::$container->get('doctrine')->getRepository(User::class)->findOneByEmail('...');
        $this->assertTrue(self::$container->get('security.password_encoder')->isPasswordValid($user, '...');
        // ...
}

Comments

That’s make our tests easier to develop ! Thanks 👍🏽
I was used to creating mock for better logic management and focus my test on the class which is linked but that's a great feature.

Just for knowledge, it that not a better idea to use the `setUp()` method and call all the services that our suites need once instead of calling them directly in the test*?
@Guillaume that's just a matter of preference. If there is nothing to share between different tests, putting things in the tests rather than in setUp is fine.
ah, sweet - i really didn't like having a separate service config for test env :)
Looks good, but would not that make test weaker? How to ensure the defined services are not public on a test?

Also, you are talking about "a special container" on `KernelTestCase` and `WebTestCase`. How to have this special container on a custom test case setup with, for example, a `KernelTestTrait`?

Thanks! :-)
Great news! thanks!
@Sullivan the special container is also a public service named "test.service_container" so you can use it anywhere in your test where you have the real container.

> How to ensure the defined services are not public on a test

The real services still get the real container, so they cannot access private services. If you need the real container in your tests themselves, you can access the real container via the kernel as usual.
@Nicolas does it mean that I will be able to mock private service in functional tests? If no, what is the best practice?

My usecase : I'm testing a controller which fetch data on an external API using Guzzle. I want to mock this Guzzle client class.
Does it work with sf 4.0.8 ?
@Jérémi I'm afraid it doesn't because Symfony always adds new features in the upcoming version (4.1 in this case) and not in the existing versions.

Comments are closed.

To ensure that comments stay relevant, they are closed for old posts.