【问题标题】:How can I mock an autowired service to symfony controller in phpunit functional tests?如何在 phpunit 功能测试中模拟自动连接到 symfony 控制器的服务?
【发布时间】:2018-11-13 00:13:56
【问题描述】:

没有问题的上下文

假设在 Symfony 3.3 中,我们有一个默认的控制器来绘制“Hello, world!”:

class DefaultController extends Controller
{
    public function indexAction() : Response
    {
        return new Response( 'Hello, world!' );
    }
}

如果我想测试它,我只是创建一个 WebTestCase 并在客户端或爬虫上做一些断言,例如

class DefaultControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request( 'GET', '/route/to/hello-world/' );

        $this->assertEquals( 200, $client->getResponse()->getStatusCode() );
        $this->assertContains( 'Hello', $crawler->filter( 'body' )->text() );
    }
}

这很好用。

有问题的上下文

假设我们有一些经过单元测试的服务。例如IdGenerator 服务,它在我们需要时根据某种算法创建新的 Id,它们只是纯文本:

class IdGenerator
{
    public function generateNewId() : string;
}

假设我们通过 autowiring 将其注入控制器。我们希望控制器会说出类似Hello, world, on request 8bfcedbe1bf3aa44e0545375f0e52f6b969c50fb! 的内容,其中这串字符来自 IdGenerator。

class DefaultController extends Controller
{
    public function indexAction( IdGenerator $idGenerator ) : Response
    {
        $id = $idGenerator->generateNewId();
        $message = sprintf( 'Hello, world, on request %s!', $id );
        return new Response( $message );
    }
}

当然,我可以在浏览器中多次重新加载页面,每次都会看到文本随着时间的推移使用新的 ID 发生变化。

但这不是自动化测试应有的方式。我们应该模拟 IdGenerator,所以它返回一个特定的id 来断言:

class DefaultControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $id = 'test-id-test-id-test-id';
        $idGenerator = $this->createMock( IdGenerator::class );
        $idGenerator->method( 'generateNewId' )->willReturn( $id );

        // What do I have to do with $idGenerator now here????

        $client = static::createClient();

        // Do something else here?

        $crawler = $client->request( 'GET', '/admin/flights/search/' );

        $this->assertEquals( 200, $client->getResponse()->getStatusCode() );
        $this->assertContains( $id, $crawler->filter( 'body' )->text() );
    }
}

我已经测试过从内核、客户端获取容器,并在那里设置服务,它确实工作:

    $id = 'test-id-test-id-test-id';
    $idGenerator = $this->createMock( IdGenerator::class );
    $idGenerator->method( 'generateNewId' )->willReturn( $id );

    $client = static::createClient();
    $kernel = $client->getKernel();
    $container = $kernel->getContainer();
    $container->set( 'My\Nice\Project\Namespace\IdGenerator', $idGenerator );

它仍然得到自动装配的,而不是我想要的(模拟)。

问题

如何设置 WebTestCase 以便自动装配连接我的模拟服务?

【问题讨论】:

  • 我已经多次看到这类问题了。 “自动化”测试从单元测试到测试最终软件的功能/集成测试,所有依赖项通常都是模拟的。您正试图通过尝试修改容器来做一些中间的事情。只是不会按照你想要的方式发生。容器已编译,您无法用新服务替换现有服务。您可以做的是严格执行 IdGenerator 服务以进行测试。但这违背了测试的目的。
  • 那么也许是设计测试的问题?我的意思是,也许我的“单元测试”可能依赖于模拟,但我的“功能测试”不应该依赖于它们?
  • 是的。这几乎涵盖了它。此外,依赖解析 html 响应会很快变得陈旧和脆弱。

标签: symfony mocking phpunit autowired functional-testing


【解决方案1】:

简短的回答是你没有。

WebTestCase 用于更高级别的测试,有时称为功能测试或集成测试。它们旨在使用实际服务或适当的测试替代方案,例如用于测试的 SQLite 数据库而不是 MySQL 或 Paypal 沙盒而不是生产服务。如果您想测试服务并用模拟或存根替换其依赖项,则应改为编写单元测试。

如果您想用虚拟实现替换您的服务,例如总是根据输入返回相同的 id 或散列,您可以替换 config/services_test.yaml 中容器配置中的别名,只要您的应用程序使用测试应用程序环境(默认情况下 WebTestCase 会这样做),就会使用该别名。您也可以尝试在运行时更改容器,但是因为 Symfony 会编译容器然后冻结它,即不允许对容器进行任何更改,这可能会很棘手并且不推荐。

作为进一步的参考,Symfony 4.1 提供了一个包含所有服务的容器,包括私有服务,暴露:https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing 这可能对您的情况没有帮助,但它显示了您如何与 WebTestCase-tests 中的服务容器进行交互。

【讨论】:

    猜你喜欢
    • 2013-11-12
    • 2021-12-23
    • 2020-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-21
    • 1970-01-01
    相关资源
    最近更新 更多