Symfony 5 : Mocking private autowired services in Controller functional tests
Tags: symfony, php, phpunit, testing
Published: 2021-02-13 05:34:27

# Why?

Example use case:

Let's say you are working on a modern Symfony application where frontend and backend are decoupled. Your application is simply a REST API which communicates with fronted by using JSON payloads. Throwing exceptions in your backend and leaving them unhandled is a bad idea. If your backend encounters an exception, you'll either end up with a crashed application or transfer the responsibility to frontend to handle the exceptions... but it's not frontend's job to do that.

So, you'll need to catch and process all exceptions in your controller action and return an appropriate error response (if an exception happens). That's easy... just wrap your code in `try / catch` and process exceptions to produce JSON error responses.

Your controller action is probably using some autowired services. These services can throw two types of exceptions:

* Exceptions which can be thrown based on user input (contents of the HTTP `Request` object)
* Exceptions which can only be thrown if your application is misconfigured and does not depend on user input (for example: missing runtime environment variables)

Testing the response of the first type of exceptions is easy - you can craft a `Request` in your test case to trigger an exception. 

But what about testing exception responses which can not be triggered by building a custom `Request` object because they don't depend on user input?

# Controller sample

```php
//...
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Service\TokenService;

class SampleController extends AbstractController
{
//...
    private TokenService $tokenService;

    public function getTokenService(): TokenService
    {
        return $this->tokenService;
    }

    public function index(): JsonResponse
    {
        try {
            $token = $this->getTokenService()->getToken();
//...
        } catch (\Throwable $exception) {
            return $this->getResponseProcessingService()->processException($exception);
        }
    }
//...
}
```
Let's assume that `getToken()` method of the `TokenService` is only capable of throwing exceptions which are not based on user input. In order to cover the `catch` code block with a test, you must force this method to throw an exception. That's why we're going to mock it.

# Solution

I'm assuming you already know hot to create a functional test for your controller actions. If you don't, read the official documentation [here](https://symfony.com/doc/current/testing.html#functional-tests)

**Test class**

```php
//...
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use App\Service\TokenService;

class SampleControllerTest extends WebTestCase 
{
    public function testIndex()
    {
        $client = static::createClient();
    // Start mocking
        $container = self::$container;
        $tokenServiceMock = $this->getMockBuilder(TokenService::class)
            ->disableOriginalConstructor()
            ->onlyMethods(['getToken'])
            ->getMock();
        $tokenServiceMock->method('getToken')->willThrowException(new \Exception());
        $container->set('App\Service\TokenService', $tokenServiceMock);
    // End mocking
        $client->request('GET', '/sample');
//...
    }
//...
}
```
So, what did we do here?

`$client = static::createClient();` and `$client->request('GET', '/sample');` are two standard lines of code in almost every functional test in Symfony. 
In order to successfully mock an autowired service, we need to create a mock and inject it into the Service Container. That needs to be done **after** booting the kernel (`static::createClient`) and **before** calling the controller action (`$client->request()`).

In addition to this, you'll also need to declare `TokenService` `public` in your test environment's service container. To do this, open `/config/services_test.yaml` and make your `TokenService` public:

```yaml
    App\Service\TokenService:
        public: true
```

All services in Symfony are private by default and unless you have a really good reason, they should stay private on any other environment except `test`. We need to make the `TokenService` public for our test, because otherwise we would get an exception when trying to set it (`$container->set()`) in our test case:
```
Symfony\Component\DependencyInjection\Exception\InvalidArgumentException : The "App\Service\TokenService" service is private, you cannot replace it.
```

# Summary

* Declare your service as `public` in test environment's service container (`/config/services_test.yaml`);
* Create a functional test client, which boots the kernel and creates a service container (`static::createClient()`);
* Create a mock of your service;
* Replace the default definition of your service with the mock dynamically (`$container->set()`);
* Run the client (`$client->request()`)

If you liked the article,...
[![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kugdh9szk858ncio2mzi.png)](https://www.buymeacoffee.com/puEW3HvWvP)