23

我有一个控制器,我想为其创建功能测试。该控制器通过类向外部 API 发出 HTTP 请求MyApiClient。我需要模拟这个类,所以我可以测试我的控制器如何响应给定的响应(例如,如果类返回 500 响应MyApiClient它将做什么)。MyApiClient

我通过标准 PHPUnit mockbuilder 创建类的模拟版本没有问题MyApiClient:我遇到的问题是让我的控制器将此对象用于多个请求。

我目前在测试中执行以下操作:

class ApplicationControllerTest extends WebTestCase
{

    public function testSomething()
    {
        $client = static::createClient();

        $apiClient = $this->getMockMyApiClient();

        $client->getContainer()->set('myapiclient', $apiClient);

        $client->request('GET', '/my/url/here');

        // Some assertions: Mocked API client returns 500 as expected.

        $client->request('GET', '/my/url/here');

        // Some assertions: Mocked API client is not used: Actual MyApiClient instance is being used instead.
    }

    protected function getMockMyApiClient()
    {
        $client = $this->getMockBuilder('Namespace\Of\MyApiClient')
            ->setMethods(array('doSomething'))
            ->getMock();

        $client->expects($this->any())
            ->method('doSomething')
            ->will($this->returnValue(500));

        return $apiClient;
    }
}

似乎在发出第二个请求时正在重建容器,导致MyApiClient再次实例化。该类MyApiClient通过注解(使用 JMS DI Extra Bundle)配置为服务,并通过注解注入到控制器的属性中。

如果可以的话,我会将每个请求拆分为自己的测试以解决此问题,但不幸的是我做不到:我需要通过 GET 操作向控制器发出请求,然后发布它返回的表单。我想这样做有两个原因:

1)表单使用CSRF保护,所以如果我直接POST到表单而不使用爬虫提交,表单没有通过CSRF检查。

2) 测试表单在提交时生成正确的 POST 请求是一种奖励。

有人对如何做到这一点有任何建议吗?

编辑:

这可以在以下不依赖于我的任何代码的单元测试中表示,因此可能更清楚:

public function testAMockServiceCanBeAccessedByMultipleRequests()
{
    $client = static::createClient();

    // Set the container to contain an instance of stdClass at key 'testing123'.
    $keyName = 'testing123';
    $client->getContainer()->set($keyName, new \stdClass());

    // Check our object is still set on the container.
    $this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.

    $client->request('GET', '/any/url/');

    $this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.

    $client->request('GET', '/any/url/');

    $this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Fails.
}

此测试失败,即使我$client->getContainer()->set($keyName, new \stdClass());在第二次调用之前立即调用request()

4

7 回答 7

9

当你调用 时self::createClient(),你会得到一个 Symfony2 内核的启动实例。这意味着,所有配置都被解析和加载。现在发送请求时,您是第一次让系统完成它的工作,对吧?

在第一个请求之后,您可能想要检查发生了什么,因此,内核处于发送请求的状态,但它仍在运行。

如果您现在运行第二个请求,网络架构要求内核重新启动,因为它已经运行了一个请求。当您第二次执行请求时,将在您的代码中执行此重新启动。

如果您想在请求发送给它之前启动内核并对其进行修改(如您所愿),您必须关闭旧的内核实例并启动一个新的实例。

你可以通过重新运行来做到这一点self::createClient()。现在你必须再次应用你的模拟,就像你第一次做的那样。

这是您的第二个示例的修改代码:

public function testAMockServiceCanBeAccessedByMultipleRequests()
{
    $keyName = 'testing123';

    $client = static::createClient();
    $client->getContainer()->set($keyName, new \stdClass());

    // Check our object is still set on the container.
    $this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName)));

    $client->request('GET', '/any/url/');

    $this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName)));

    # addded these two lines here:
    $client = static::createClient();
    $client->getContainer()->set($keyName, new \stdClass());

    $client->request('GET', '/any/url/');

    $this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName)));
}

现在您可能想要创建一个单独的方法,为您模拟新实例,因此您不必复制代码......

于 2013-11-13T10:29:18.403 回答
8

我以为我会跳到这里。Chrisc,我想你想要的是这里:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

我同意您的一般方法,在服务容器中将其配置为参数确实不是一个好方法。整个想法是能够在单独的测试运行期间动态地模拟它。

于 2013-07-03T17:47:44.073 回答
2

您所遇到的行为实际上是您在任何真实场景中所经历的,因为 PHP 不共享任何内容并在每个请求上重建整个堆栈。功能测试套件模仿这种行为不会产生错误的结果。一个示例是具有 ObjectCache 的教条,因此您可以创建对象,而不是将它们保存到数据库中,并且您的测试将全部通过,因为它始终将对象从缓存中取出。

您可以通过不同的方式解决此问题:

创建一个真正的类,它是一个 TestDouble 并模拟您期望从真正的 API 获得的结果。这实际上非常简单:您创建一个MyApiClientTestDouble与您的 normal 具有相同签名的新MyApiClient方法,并在需要的地方更改方法主体。

在你的 service.yml 中,你可能有这个:

parameters:
  myApiClientClass: Namespace\Of\MyApiClient

service:
  myApiClient:
    class: %myApiClientClass%

如果是这种情况,您可以通过将以下内容添加到 config_test.yml 来轻松覆盖所采用的类:

parameters:
  myApiClientClass: Namespace\Of\MyApiClientTestDouble

现在服务容器将在测试时使用您的 TestDouble。如果两个类具有相同的签名,则不需要更多。我不知道这是否或如何与 DI Extras Bundle 一起使用。但我想有办法。

或者您可以创建一个 ApiDouble,实现一个“真实”API,其行为方式与您的外部 API 相同,但返回测试数据。然后,您将使 API 的 URI 由服务容器处理(例如 setter 注入)并创建一个指向正确 API 的参数变量(在开发或测试的情况下为测试,在生产环境的情况下为真实的)。

第三种方式有点笨拙,但您始终可以在测试request中创建一个私有方法,该方法首先以正确的方式设置容器,然后调用客户端发出请求。

于 2013-03-11T22:05:39.763 回答
2

我不知道你是否知道如何解决你的问题。但这是我使用的解决方案。这对其他人也有好处。

经过长时间搜索在多个客户端请求之间模拟服务的问题后,我发现了这篇博文:

http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html

lyrixx 讨论了内核如何在每次请求后关闭,从而在您尝试发出另一个请求时使服务覆盖无效。

为了解决这个问题,他创建了一个仅用于功能测试的 AppTestKernel。

这个 AppTestKernel 扩展了 AppKernel 并且只应用了一些处理程序来修改内核:来自 lyrixx 博客文章的代码示例。

<?php

// app/AppTestKernel.php

require_once __DIR__.'/AppKernel.php';

class AppTestKernel extends AppKernel
{
    private $kernelModifier = null;

    public function boot()
    {
        parent::boot();

        if ($kernelModifier = $this->kernelModifier) {
            $kernelModifier($this);
            $this->kernelModifier = null;
        };
    }

    public function setKernelModifier(\Closure $kernelModifier)
    {
        $this->kernelModifier = $kernelModifier;

        // We force the kernel to shutdown to be sure the next request will boot it
        $this->shutdown();
    }
}

然后,当您需要在测试中覆盖服务时,您调用 testAppKernel 上的设置器并应用模拟

class TwitterTest extends WebTestCase
{
    public function testTwitter()
    {
        $twitter = $this->getMock('Twitter');
        // Configure your mock here.
        static::$kernel->setKernelModifier(function($kernel) use ($twitter) {
            $kernel->getContainer()->set('my_bundle.twitter', $twitter);
        });

        $this->client->request('GET', '/fetch/twitter'));

        $this->assertSame(200, $this->client->getResponse()->getStatusCode());
    }
}

遵循本指南后,我在使用新的 AppTestKernel 启动 phpunittest 时遇到了一些问题。

我发现 symfonys WebTestCase ( https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php ) 采用它找到的第一个 AppKernel 文件。因此,摆脱这种情况的一种方法是将 AppTestKernel 上的名称更改为在 AppKernel 之前或覆盖该方法以取而代之 TestKernel

在这里,我覆盖了 WebTestCase 中的 getKernelClass 以查找 *TestKernel.php

    protected static function getKernelClass()
  {
            $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();

    $finder = new Finder();
    $finder->name('*TestKernel.php')->depth(0)->in($dir);
    $results = iterator_to_array($finder);
    if (!count($results)) {
        throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
    }

    $file = current($results);

    $class = $file->getBasename('.php');

    require_once $file;

    return $class;
}

在此之后,您的测试将使用新的 AppTestKernel 加载,您将能够在多个客户端请求之间模拟服务。

于 2013-10-31T11:24:29.430 回答
2

根据 Mibsen 的回答,您还可以通过扩展 WebTestCase 并覆盖 createClient 方法以类似的方式进行设置。这些方面的东西:

class MyTestCase extends WebTestCase
{
    private static $kernelModifier = null;

    /**
     * Set a Closure to modify the Kernel
     */
    public function setKernelModifier(\Closure $kernelModifier)
    {
        self::$kernelModifier = $kernelModifier;

        $this->ensureKernelShutdown();
    }

    /**
     * Override the createClient method in WebTestCase to invoke the kernelModifier
     */
    protected static function createClient(array $options = [], array $server = [])
    {
        static::bootKernel($options);

        if ($kernelModifier = self::$kernelModifier) {
            $kernelModifier->__invoke();
            self::$kernelModifier = null;
        };

        $client = static::$kernel->getContainer()->get('test.client');
        $client->setServerParameters($server);

        return $client;
    }
}

然后在测试中你会做类似的事情:

class ApplicationControllerTest extends MyTestCase
{
    public function testSomething()
    {
        $apiClient = $this->getMockMyApiClient();

        $this->setKernelModifier(function () use ($apiClient) {
            static::$kernel->getContainer()->set('myapiclient', $apiClient);
        });

        $client = static::createClient();

        .....
于 2015-10-01T04:28:01.083 回答
0

我在 Symfony 4.4 中遇到了同样的问题。

在阅读 使用 Symfony 在 API 功能测试中创建模拟后, 我找到了一个解决方案 -self::ensureKernelShutdown()

...    
$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.

self::ensureKernelShutdown()

$client->request('GET', '/any/url/');
$this->assertEquals('stdClass', get_class($client->getContainer()->get($keyName))); // Passes.
...
于 2020-07-21T19:38:26.060 回答
0

做一个模拟:

$mock = $this->getMockBuilder($className)
             ->disableOriginalConstructor()
             ->getMock();

$mock->method($method)->willReturn($return);

替换模拟对象上的 service_name:

$client = static::createClient()
$client->getContainer()->set('service_name', $mock);

我的问题是使用:

self::$kernel->getContainer();
于 2017-05-25T16:24:10.520 回答