问题
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 ApiClient
to 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