如何在功能测试中使用symfony 4模拟服务?

时间:2018-05-28 21:34:00

标签: symfony service dependency-injection functional-testing

我有一个命令总线处理程序,它会注入一些服务:

class SomeHandler
{
    private $service;

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

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

SomeService有方法doSomeStuff和外部调用,我不想在测试期间使用它。

class SomeService
{
    private $someBindedVariable;

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

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

在测试中我尝试用模拟对象替换服务

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”App \ Service \ SomeService“服务是私有的,你不能替换它。”

好的,让我们试着把它公之于众:

services.yaml:

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

但它没有帮助。我从本机SomeService获得响应。让我们试试别名:

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

App\Service\SomeService:
    alias: some_service

同样,模拟对象不会通过测试使用。我看到来自本地SomeService的回复。

我试图附加autowire选项,但它没有帮助。

我应该怎么做才能在测试期间用整个项目中的一些模拟对象替换我的SomeService?

3 个答案:

答案 0 :(得分:2)

最好的方法是明确定义该服务并让它具有参数,以便您注入的类可以基于环境参数(因此,基本上,为测试和开发定义不同的实现)你试图注入的服务。)

基本上,像

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

然后,您可以在common.yaml中为FQCN定义实际实现

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

并在test.yaml中(应该在 common.yaml后加载以覆盖它)存根实现

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

您无需触及任何代码行或任何测试文件行即可完成:只需配置。

注意

这只是一个概念性和说明性示例,您应该根据代码进行调整(例如,请参阅common.yamltest.yaml)以及您的类名称(请参阅yourService和所有的FQCN事情)

此外,在symfony 4中,由于配置文件可以被.env文件替换以获得相同的结果,因此您只需要调整这些概念。

答案 1 :(得分:2)

要模拟和测试服务,可以添加以下配置:

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'

答案 2 :(得分:1)

我在嘲笑上有一个完全相同的问题。但就我而言,我需要模拟ApiClient以避免在测试环境中进行真正的API调用,并且需要使此模拟可配置,以便能够在实际环境中根据每个测试用例模拟任何请求/响应时间或添加几对请求/响应,因为某些情况下需要针对单个功能测试用例的多个API调用到不同的端点。以前的方式非常容易实现和读取:

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

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

因此,在上面的代码中,我可以为每个测试用例添加任意数量的实现。但是现在在Symfony中无法正常工作。我不知道为什么他们不能像在这里https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing那样为测试环境创建一个容器,而且还具有实时设置服务的能力(我相信他们不能这样做是有原因的,对此不是很熟悉)。

我能创建不同的服务实现并将其添加到services_test.yml(此配置文件在测试时正在读取,因此您无需将类名作为env参数传递),并在测试env中使用它,但是我仍然无法在每个测试用例的基础上增加期望,我所能做的只是对该实现中的所有期望进行硬编码,但是对我而言,它的肮脏解决方案以及创建模拟的简单任务变得非常头疼,因为您需要创建很多代码只是为了替换某些类的功能,这使创建测试的过程变得复杂,但是我认为它应该很简单