如何在Symfony 4功能测试中模拟服务?

13

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

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" service is private, you cannot replace it."

好的,让我们尝试将其改为公共服务:

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中看到了响应。

我尝试添加自动装配选项,但并没有帮助。

在整个项目的测试中,我该如何将我的SomeService替换为某个模拟对象?


这仍然是一个相关问题,有人找到了单个测试中模拟服务的适当解决方案吗? - M. Hardy
3个回答

5

为了模拟和测试服务,您可以使用以下配置:

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'

3
请您能详细说明一下吗? - Xavi Montero

4

最好的方法是明确定义服务,并使其参数化,以便注入的类可以基于环境参数进行定义(因此,基本上为您正在尝试注入的服务定义测试和开发的不同实现)。

基本上,就像这样:

<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文件替换以达到相同的结果,因此您只需要适应这些概念即可。


我想指出,在S4中,基于环境的配置文件仍然存在。我也有点怀疑你的方法是否允许使用模拟对象。另一方面,我对OP尝试的整个方法持怀疑态度。 - Cerad
@Cerad 我不确定那些配置文件。我进行了编辑。只要模拟对象是真正返回某些东西的对象,我的方法就能起作用,所以不完全是真正的模拟,而更像存根。我在集成/端到端测试中使用它们,并且必须承认,只需几乎零代码,我就可以获得OP所要求的内容。你为什么对OP的请求表示怀疑呢? - DonCallisto
我理解这个问题的方式是,他们想要使用一个真正的模拟对象。并且他们希望这个对象替换容器中现有的对象。我可能错了。 - Cerad
@Cerad,你说得对,只要他们想要一个真正的模拟而不是存根。你知道,存根/模拟通常被(错误地)用作同义词。我的理解是,他们需要“伪造”一些东西,以便不调用实际实现,因此不关注所调用的内容,而是关注将返回什么。我不知道我是否正确,但我非常确定一点:在这种情况下更好的方法是提供可配置的内容来更改服务。如果他们需要一个模拟,我建议使用setter注入,但不知道它如何与自动装配一起使用(实际上我看不到任何问题)。 - DonCallisto

3

我遇到了一个与模拟相关的问题。但在我的情况下,我需要模拟 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 中(此配置文件在测试过程中被读取,因此您不需要将类名作为环境参数传递),并在测试环境中使用它,但我仍然无法按照每个测试用例的基础添加期望,我只能在该实现中硬编码所有期望,但这对我来说是一个肮脏的解决方案,并且创建模拟对象变成了一个真正的头疼问题,因为您需要创建大量的代码来替换某些类的功能,这使得创建测试的过程变得复杂,但我认为这应该是一项直截了当的任务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接