5

How can I mock a service in a functional test use-case where a "request"(form/submit) is being made. After I make the request all the changes and mocking I made to the container are lost.

I am using Symfony 4 or 5. The code posted here can be also found here: https://github.com/klodoma/symfony-demo

I have the following scenario:

  • SomeActions service is injected into the controller constructor
  • in the functional unit-tests I try to mock the SomeActions functions in order to check that they are executed(it sends an email or something similar)

I mock the service and overwrite it in the unit-tests:

$container->set('App\Model\SomeActions', $someActions);

Now in the tests I do a $client->submit($form); which I know that it terminates the kernel.

My question is: HOW can I inject my mocked $someActions in the container after $client->submit($form);

Below is a sample code I added to the symfony demo app https://github.com/symfony/demo

enter image description here

in services.yaml

App\Model\SomeActions:
    public: true

SomeController.php

<?php

namespace App\Controller;

use App\Model\SomeActions;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Controller used to send some emails
 *
 * @Route("/some")
 */
class SomeController extends AbstractController
{
    private $someActions;

    public function __construct(SomeActions $someActions)
    {
        //just dump the injected class name
        var_dump(get_class($someActions));

        $this->someActions = $someActions;
    }

    /**
     * @Route("/action", methods="GET|POST", name="some_action")
     * @param Request $request
     * @return Response
     */
    public function someAction(Request $request): Response
    {

        $this->someActions->doSomething();

        if ($request->get('send')) {
            $this->someActions->sendEmail();
        }

        return $this->render('default/someAction.html.twig', [
        ]);
    }
}

SomeActions

<?php

namespace App\Model;

use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class SomeActions
{
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function doSomething()
    {
        echo 'doSomething';
    }

    public function sendEmail()
    {
        echo 'sendEmail';

        $email = (new Email())
            ->from('hello@example.com')
            ->to('you@example.com')
            ->subject('Time for Symfony Mailer!')
            ->text('Sending emails is fun again!')
            ->html('<p>See Twig integration for better HTML integration!</p>');
        $this->mailer->send($email);
    }

}

SomeControllerTest.php

<?php

namespace App\Tests\Controller;

use App\Model\SomeActions;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class SomeControllerTest extends WebTestCase
{
    public function testSomeAction()
    {
        $client = static::createClient();

        // gets the special container that allows fetching private services
        $container = self::$container;


        $someActions = $this->getMockBuilder(SomeActions::class)
            ->disableOriginalConstructor()
            ->getMock();

        //expect that sendEmail will be called
        $someActions->expects($this->once())
            ->method('sendEmail');

        //overwrite the default service: class: Mock_SomeActions_e68f817a
        $container->set('App\Model\SomeActions', $someActions);


        $crawler = $client->request('GET', '/en/some/action');

        //submit the form
        $form = $crawler->selectButton('submit')->form();

        $client->submit($form);

        //after submit the default class injected in the controller is "App\Model\SomeActions" and not the mocked service
        $response = $client->getResponse();

        $this->assertResponseIsSuccessful($response);

    }
}
klodoma
  • 4,181
  • 1
  • 31
  • 42
  • Does this answer help you? https://stackoverflow.com/a/19726963/3545751 (Notice last sentence) – Beniamin Mar 02 '20 at 15:32
  • Not really, what is the solution for it? – klodoma Mar 02 '20 at 15:36
  • Its just my guessing - but you probably need to inject mocked service twice (before getting the page and before submit the form). – Beniamin Mar 02 '20 at 15:43
  • 1
    ```$client->disableReboot();$client->submit($form);``` If I do this, then I kind of achieve what I wanted. Not sure if it's the best way to do it. – klodoma Mar 02 '20 at 16:16

1 Answers1

8

The solution is to disable the kernel reboot:

$client->disableReboot();

It makes sense if ones digs deep enough to understand what's going on under the hood; I am still not sure if there isn't a more straight forward answer.

public function testSomeAction()
{
    $client = static::createClient();
    $client->disableReboot();
...
klodoma
  • 4,181
  • 1
  • 31
  • 42
  • 1
    Also, changes to the container prior to `static::createClient()` seem to get lost, so the mocking must be done after client creation. – igneus Aug 10 '20 at 11:28
  • Amazing, I've been looking for this answer for 2 days, thank you very much ! @klodoma Can you please tell me what would be the drawback to using disableReboot all the time ? Thank you again ! – lucasfoufou Jun 29 '21 at 16:46
  • @lucasfoufou It's a good question. You want a "clean" system when running your tests, like you could do something in the setup of the project or mock some services and this could lead to unwanted results. On the other hand, like in the upper example, the client MUST stay as it is. Not sure if this the best answer to your question, didn't dig into it too much. – klodoma Jul 06 '21 at 12:27