How to mock service with symfony 4 in functional tests?

…衆ロ難τιáo~ 提交于 2020-06-10 14:54:54

问题


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?


回答1:


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.




回答2:


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




回答3:


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'


来源:https://stackoverflow.com/questions/50573624/how-to-mock-service-with-symfony-4-in-functional-tests

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!