New in Symfony 4.1: Simpler service testing
March 20, 2018 • Published by Javier Eguiluz
Warning: This post is about an unsupported Symfony version. Some of this information may be out of date. Read the most recent Symfony Docs.
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, '...');
// ...
}
Keep in mind that, because of how Symfony's service container work, unused
services are removed from the container. This means that if you have a private
service not used by any other service, Symfony will remove it and you won't be
able to get it as explained in this article. The solution is to define the
service as public
explicitly so Symfony doesn't remove it.
Help the Symfony project!
As with any Open-Source project, contributing code or documentation is the most common way to help, but we also have a wide range of sponsoring opportunities.
Comments are closed.
To ensure that comments stay relevant, they are closed for old posts.
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*?
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! :-)
> 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.
My usecase : I'm testing a controller which fetch data on an external API using Guzzle. I want to mock this Guzzle client class.