Symfony 5 : Mocking private autowired services in Controller functional tests

nikolastojilj12

Nikola Stojiljkovic

Posted on February 13, 2021

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

💖 💪 🙅 🚩
nikolastojilj12
Nikola Stojiljkovic

Posted on February 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related