How to test a private service in Symfony
Tags: symfony, php, tdd, testing
Published: 2023-10-04 15:30:00

In Symfony 3.4, they made all services private by default, which means you can no longer call `$this->get('my_service_id')` in your controllers to quickly obtain a service.

This change was made because **direct usage of services from the container is considered a bad practice**. That's why controllers allow services to be injected using [Type Hinting](https://symfony.com/doc/current/service_container.html#fetching-and-using-services 'Type Hinting') in their methods and constructors. 

The only remaining inconvenience is that when running tests, we get the following: 

```shell
> bin/phpunit
# Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The "App\Service\S" 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.
``` 

### Problem 

We want to create a service and test it before integrating it with the rest of the project (standard [**TDD**](https://en.wikipedia.org/wiki/Test-driven_development 'test-driven development') approach).

I have a repository `R` that implements the `RInterface` interface. The `RInterface` interface is used in the `S` service (type hinted constructor). The `S` service is used in the `C` controller (again, as a constructor parameter).

When we run the test for the `S` service... 

```php
class STest extends KernelTestCase
{
    public function testGetItems(): void
    {
        self::bootKernel();
        $container = self::$kernel->getContainer();

        $service = $container->get(S::class);
        // ... other
    }
}
```

We see the following message:

```shell
> bin/phpunit
# Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: The "App\Service\S" 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.
```

### Reason

The service can be integrated when [certain conditions are met](https://github.com/symfony/dependency-injection/blob/6.1/Compiler/InlineServiceDefinitionsPass.php#L158 'Checks if the definition is inlineable').

To check if the `S` service is present in the test container, we type the following command:

```shell
> bin/console debug:container 'App\Service\S' --env=test
```

Here is the message we will see:

```shell
Information for Service "App\Core\Service\S"
=========================================================

 ---------------- ---------------------------------
  Option           Value
 ---------------- ---------------------------------
  Service ID       App\Service\S
  Class            App\Service\S
  Tags             -
  Public           no
  Synthetic        no
  Lazy             no
  Shared           yes
  Abstract         no
  Autowired        yes
  Autoconfigured   yes
 ---------------- ---------------------------------
```

As mentioned in the received message when running the test, the `S` service appears as private (`Public: no`). It's important to note that due to how the Symfony container works, **unused services are removed from the container**. This means that if you have a private service not used by any other service, Symfony removes it, and you cannot retrieve it from the container.

### Solution

The solution is to explicitly define the `S` service as `public` so that Symfony does not remove it. The most appropriate solution would be to create a public alias only in the test environment for the service you want to test.

```yaml
# config/services_test.yaml
services:
  test_alias.service:s:
    alias: 'App\Service\S'
    public: true
```

> Good work 👨‍💻