How to mock service with symfony 4 in functional tests?
Asked Answered
Q

3

13

I have an commandbus handler, which injects some service:

class SomeHandler
{
    private $service;

    public function __construct(SomeService $service)
    {
        $this->service = $service;
    }

    public test(CommandTest $command)
    {
        $this->service->doSomeStuff();
    }
}

SomeService has method doSomeStuff with external calls, which I want not to use during testing.

class SomeService
{
    private $someBindedVariable;

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

    public function doSomeStuff()
    {
        //TODO: some stuff
    }
}

There is in the test I try to replace service with mock object

public function testTest()
{
    $someService = $this->getMockBuilder(SomeService::class)->getMock();
    $this->getContainer()->set(SomeService::class, $someService);

    //TODO: functional test for the route, which uses SomeHandler
}

The first problem is this code will throws exception "The "App\Service\SomeService" service is private, you cannot replace it."

Ok, let's try to make it public:

services.yaml:

App\Service\SomeService:
    public: true
    arguments:
        $someBindedVariable: 200

But it doesn't help. I get response from native SomeService. Let's try with aliases:

some_service:
    class: App\Service\SomeService
    public: true
    arguments:
        $someBindedVariable: 200

App\Service\SomeService:
    alias: some_service

And again the mock object does not use by test. I see response from native SomeService.

I tried to append autowire option, but it did not help.

What should I do to replace my SomeService with some mock object all over the project during test?

Quintuplicate answered 28/5, 2018 at 21:34 Comment(1)
Still a relevant issue, has anybody found a proper solution to mock a service in a single test?Cyclops
D
5

To mock and test services you can put this configuration :

config/packages/test/service.yaml

'test.myservice_mock':
    class: App\Service\MyService
    decorates: App\Service\MyService
    factory: ['\Codeception\Stub', makeEmpty]
    arguments:
      - '@test.myservice_mock.inner'
Dioscuri answered 11/5, 2020 at 9:48 Comment(1)
Could you please elaborate more?Inflexion
F
4

Best way to do that is to defined explicitly that service and let it be parametric so the class you're injecting in could be based on the environment parameters (so, basically, define a different implementation for test and dev of the service you're trying to inject).

Basically, something like

<service id="yourService">
  <argument type="service" id="%parametric.fully.qualified.class.name%" />
</service>

Then you can define in common.yaml a FQCN for the real implementation

parameters:
    parametric.fully.qualified.class.name: Fully\Qualified\Class\Name\Real\Impl

and in test.yaml (that should be loaded after common.yaml in order to override it) a stub implementation

parameters:
    parametric.fully.qualified.class.name: Fully\Qualified\Class\Name\Stub\Impl

And you're done without touching any line of code or any line of test file: just configuration.

Pay attention

This is only a conceptual and illustrative example, you should adapt it to your code (see the common.yaml and test.yaml for instance) and to your class names (see the yourService and all the FQCN things)

Moreover in symfony 4 as config files can be replaced by the .env file in order to obtain the same result, you just need to adapt these concepts.

Forbiddance answered 29/5, 2018 at 11:1 Comment(4)
Just wanted to point out that environment based config files are alive an well in S4. I'm also a bit dubious that your approach will allow using a mock object. On the other hand, I'm a bit dubious of the whole approach the op is trying.Chaoan
@Chaoan I wasn't sure about those config files. I made an edit. My approach is working as long as mock objects are real objects returning something, so not mocks in the actual sense but more like stubs. I used them a lot in my ingegration/e2e tests and have to admin that with nearly zero code I can obtain what OP is asking for. Why are you doubtful about OP request?Forbiddance
The way I read the question, they want to use an actual mocked object. And they want this object to replace an existing object in the container. I could be wrong.Chaoan
@Chaoan you're right as long as they want a real mock and not a stub. You know, stub/mock are often (wrongly) used as synonyms. What I understand is they need to "fake" something in order to not call actual implementation so no attention on what is invoked but on what will be returned. Don't know if I'm right but that's a point I'm pretty sure: to change services in these kind of scenarios is better to provide something configurable. If they need a mock I suggest would suggest setter injection but don't know how it works with autowire (I can't see any issue actually)Forbiddance
I
3

I have a pretty same issue with mocking. But in my case, I need to mock ApiClientto avoid making real API calls in the test env and I need to make this mock configurable, to have ability mock any request/response on per test case basis in the real-time or to add a few pairs of request/response as some cases requiring multiple API calls to different endpoints for single functional test case. The way it was before was very easy to implement and read:

 $apiClientMock = \Mockery::mock(HttpClientInterface::class);
 $apiClientMock
            ->shouldReceive('send')
            ->with($request)/            
            ->andReturn(new Response(HttpCode::OK, [], '{"data":"some data"}'))
            ->once();

 $I->replaceSymfonyServiceWithMock($apiClientMock, HttpClientInterface::class);

so in a code above, I can add as many realizations I want per test case. But now in Symfony is not working. I do not know why they can't make a container for test env as they made here https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing but also with the ability to set services in real time (I believe there is a reason they can't, I'm not very familiar with this).

What I can is to create different service realization and add it to services_test.yml (this config file is reading while tests so you do not need to pass class name as an env parameter), and use it in the testing env, but I still can't add expectation to it on per test case basis, what I can is just hardcode all expectation in that implementation but its dirty solution as for me, and simple task as creating mock become a real headache as you need to create a lot of code just to replace some class functionality and it makes process of creating tests complicated, but I think it should be straightforward

Inferno answered 6/11, 2018 at 10:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.