unit-testingsymfonyphpunitsymfony5symfony-eventdispatcher

Accessing request container (event_dispatcher) within a test client


I created a simple test case in Symfony. So one client which should listen for an event which will be dispatched during an request. But nothing happen because the request have an own scope or I dont know why Im not able to access the dispatcher in it.

$this->client = static::createClient();
self::$container = $this->client->getContainer();

$dispatcher = self::$container->get('event_dispatcher');

$dispatcher->addListener('example', function ($event) {
    // Never executed
});

$this->client->request('POST', $endpoint, $this->getNextRequestParameters($i), [$file], $this->requestHeaders);
$this->client->getResponse();

The listener is never called. When I debug it a bit I find out that the object hash via spl_object_hash($dispatcher) is different on the highest level than on within the request level. So it seems that the request has an own world and ignores everything outside.

But then is the question how I can put my listener to this "world"?


Solution

  • I think part of the problem is the mixing of testing styles. You have a WebTestCase which is intended for a very high level of testing (requests & responses). It should not really care about internals, i.e. which services or listeners are called. It only cares that given input x (your request) you will get output y (your response). This allows to ensure the basic functionality as perceived by your users is always met, without caring how it is done. Making these tests very flexible.

    By looking into the container and the services you are going into a lower level of testing, which tests interconnected services. This is usually only done within the same process for the reasons you already found out. The higher level test has 2 separate lifecycles, one for the test itself and one for the simulated web request to your application, hence the different object ids.

    The solution is either to emit something to the higher level, e.g. by setting headers or changing the output, so you can inspect the response body. You could also write into some log file and check the logs before/after the request for that message.

    A different option would be to move the whole test into a lower level where you do not need the requests and instead only work with the services. For this you can use the KernelTestCase (instead of the WebTestCase) and instead of calling createClient() you call bootKernel. This will give you access to your container where you can modify the EventDispatcher. Rather than sending a request you can then either call the code directly, e.g. dispatch an event if you only want to test the listeners, or you can make your controller accessible as service and then manually create a request, call the action and then either check the response or whatever else you want to assert on. This could look roughly like this:

    public function testActionFiresEvent()
    {
        $kernel = static::bootKernel();
    
        $eventDispatcher = $kernel->getContainer()->get('event_dispatcher');
    
        // ...
    
        $request = Request::create();
    
        // This might not work when the controller
        // You can create a service configuration only used by tests,
        // e.g. "config/services_test.yaml" and provide the controller service there
        $controller = $kernel->getContainer()->get(MyController::class);
    
        $response = $controller->endpointAction($request);
    
        // ...Do assertions...
    }