Testing Symfony 5 applications - Injecting a mock into the service container

Article Index

Injecting a mock into the service container

When doing system testing, sometime you need to change one of the services used by a controler class. Usually you want to use all the services when calling a page in your project. But sometimes it is necessary to change the behavior of a service, e.g. when an error situation should be tested, like a service which throws an exception. Doing this is quite easy:

  • Mock the service whose behaviour you want to change.
  • Define the behaviour, e.g throw an exception when a certain method is called.
  • Inject the mocked service into the service container which will be used by the controller class when being called by the test browser, i.e. the crawler.

While steps 1 and 2 are straight-forward, step 3 can be a real pain. There are two ways to overwrite an existing service in the container. Because of the way the service container is build in Symfony, this is only possible for public services. So in the services.yaml, you need to define the service to be mocked like this:

services:
    ...

    App\Service\GenerateExcelDataFile:
        public: true

Using the test container

In Symfony 3 most services were made private, which made it impossible to overwrite them with mocks in tests. Since Symfony 4.1 there is a special container for tests. How to use this, is described in https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing:

<?php

namespace App\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MyClassTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $serviceMock = $this->createMock(MyService::class);
        $serviceMock->method('doSomeThing')
                      ->will($this->throwException((new \Exception('Testexception'))));

        self::$container->set(MyService::class, $serviceMock);
        $crawler = $client->request('GET', '/index');
        $this->assertStringContainsString('Textexception', $crawler->html());
    }
}

In line 17, the service which is used by the controller is injected into the container. When calling the controller in line 18, it uses the mock service, which throws an exception.

Using the container from the client

In system tests in Symfony, you create a client object, which is uses to browse the pages. You can get the container object and inject the mock into this object. Here is a small example:

<?php

namespace App\Tests;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MyClassTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $serviceMock = $this->createMock(MyService::class);
        $serviceMock->method('doSomeThing')
                      ->will($this->throwException((new \Exception('Testexception'))));

        $client->getContainer()->set(MyService::class, $serviceMock);
        $crawler = $client->request('GET', '/index');
        $this->assertStringContainsString('Textexception', $crawler->html());
    }
}

In line 17, the service which is used by the controller is injected into the container. When calling the controller in line 18, it uses the mock service, which throws an exception.