4

Non-problematic context

Let's say in Symfony 3.3 we have a default controller that paints "Hello, world!":

class DefaultController extends Controller
{
    public function indexAction() : Response
    {
        return new Response( 'Hello, world!' );
    }
}

If I want to test it, I just create a WebTestCase and do some assertions on the client or the crawler, for example

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

        $crawler = $client->request( 'GET', '/route/to/hello-world/' );

        $this->assertEquals( 200, $client->getResponse()->getStatusCode() );
        $this->assertContains( 'Hello', $crawler->filter( 'body' )->text() );
    }
}

This just works fine.

Problematic context

Let's say we have some unit-tested services. For example a IdGenerator service that creates new Ids based on a certain algorithm when we need one, and they are just plain text:

class IdGenerator
{
    public function generateNewId() : string;
}

Say we I inject it via autowiring into the controller. And we expect that the controller says something like Hello, world, on request 8bfcedbe1bf3aa44e0545375f0e52f6b969c50fb! where this bunch of characters come from the IdGenerator.

class DefaultController extends Controller
{
    public function indexAction( IdGenerator $idGenerator ) : Response
    {
        $id = $idGenerator->generateNewId();
        $message = sprintf( 'Hello, world, on request %s!', $id );
        return new Response( $message );
    }
}

Of course I could reload the page multiple times in a browser and see the text changing over time with new Ids each time.

But this is not the way automated tests should world. We should mock the IdGenerator, so it returns a specific id to assert:

class DefaultControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $id = 'test-id-test-id-test-id';
        $idGenerator = $this->createMock( IdGenerator::class );
        $idGenerator->method( 'generateNewId' )->willReturn( $id );

        // What do I have to do with $idGenerator now here????

        $client = static::createClient();

        // Do something else here?

        $crawler = $client->request( 'GET', '/admin/flights/search/' );

        $this->assertEquals( 200, $client->getResponse()->getStatusCode() );
        $this->assertContains( $id, $crawler->filter( 'body' )->text() );
    }
}

I have already tested to get the container, from the kernel, from the client, and set the service there and it does not work:

    $id = 'test-id-test-id-test-id';
    $idGenerator = $this->createMock( IdGenerator::class );
    $idGenerator->method( 'generateNewId' )->willReturn( $id );

    $client = static::createClient();
    $kernel = $client->getKernel();
    $container = $kernel->getContainer();
    $container->set( 'My\Nice\Project\Namespace\IdGenerator', $idGenerator );

It still gets the autowired one instead of the one I want (the mock).

Question

How can I setup the WebTestCase so the autowiring wires my mocked service?

Xavi Montero
  • 9,239
  • 7
  • 57
  • 79
  • 1
    I have seen these sorts of questions several times. "Automated" testing runs the gamut from unit testing in which all dependencies are generally mocked and functional/integration tests which test the final software. You are trying to do something in the middle by trying to modify the container. Just not going to happen the way you want it to. The container is compiled and you can't replace an existing service with a new one. What you might be able to do is make an IdGenerator service strictly for testing. But that sort of defeats the purpose of testing. – Cerad Jun 03 '18 at 14:37
  • So maybe it's a problem at designing the tests? I mean, maybe my "unit-tests" could depend on mocks, but my "functional-tests" should never depend on them? – Xavi Montero Jun 03 '18 at 15:56
  • 1
    Yep. That pretty much covers it. Besides, relying on parsing html responses gets old and fragile real fast. – Cerad Jun 03 '18 at 16:51

1 Answers1

5

The short answer is you don't.

The WebTestCase is intended for higher level tests, sometimes called Functional or Integration tests. They are intended to use the actual services or appropriate test alternatives, e.g. a SQLite database for tests instead of MySQL or a Paypal-sandbox instead of the production service. If you want to test a service and replace its dependencies with a mock or stub you should be writing a Unit Test instead.

If you want to replace your service with a dummy implementation, e.g. one that always returns the same id or a hash based on the input, you can replace the alias in the container configuration in your config/services_test.yaml that will be used whenever your application uses the test app environment (which the WebTestCase by default does). You could also try changing the container during runtime, but because Symfony compiles the container and then freezes it, i.e. does not allow any changes to the container, it might be tricky and is not really recommended.

As a further reference Symfony 4.1 provides a container with all services, including private ones, exposed: https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing This will likely not help in your case, but it shows how you can interact with the service container in WebTestCase-tests.

dbrumann
  • 16,803
  • 2
  • 42
  • 58