DEV Community

Nikola Stojiljkovic
Nikola Stojiljkovic

Posted on • Updated on

Symfony 5 : Mocking private autowired services in Controller functional tests

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

//...
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);
        }
    }
//...
}
Enter fullscreen mode Exit fullscreen mode

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

Test class

//...
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');
//...
    }
//...
}
Enter fullscreen mode Exit fullscreen mode

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:

    App\Service\TokenService:
        public: true
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

Top comments (3)

Collapse
 
florimondmanca profile image
Florimond Manca • Edited

Thanks a lot for the write-up! Did you figure out how to do this in Symfony 6? The client doesn't seem to use the updated service, unfortunately.

Edit: I solved it. My particular use case was making MyService that uses an HTTP client throw a particular kind of exception. MyService throws exceptions based on responses received by the HTTP client, so initially I wanted to mock MyService so that it throws the exception itself. But I did have a scoped mock in place for my HTTP client (similar to strangebuzz.com/en/blog/simple-api...), so I ended up making the HTTP client do the throwing instead based on a particular request input (bound to the form I'm testing). Works well.

Collapse
 
nikolastojilj12 profile image
Nikola Stojiljkovic

I haven't used this on Symfony 6, because I've switched to NodeJS for my full time job a while ago (not my decision :) ). Will find some free time to take a look.

Collapse
 
mihaicraita profile image
Mihai Craita

Thanks! really helped me by using Repository as a service I can now mock it easily in tests