Symfony 2与模拟服务功能测试

问题描述:

我有一个控制器我想创建功能测试。该控制器通过MyApiClient类向外部API发送HTTP请求。我需要嘲笑这个MyApiClient类,所以我可以测试我的控制器如何响应给定的响应(例如,如果MyApiClient类返回500响应,它会执行什么操作)。Symfony 2与模拟服务功能测试

我没有任何问题通过标准的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操作向控制器发出请求,然后POST它的形式带回来。我想这样做的原因有两个:

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. 
} 

此测试失败,即使我立即第二次调用request()

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

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

我同意你的一般方法,在作为参数的服务容器配置这实在不是一个好办法。整个想法是能够在单个测试运行期间动态地嘲笑它。

+0

谢谢你。我没有试过这个库 - 我最终只用了一个由Symfony的DI确定的单个模拟类,这并不理想 - 但我肯定会考虑将来使用它。这似乎是最适合我原来的问题,所以我接受了这个答案。 – ChrisC 2014-06-11 18:40:37

行为遇到前打电话$client->getContainer()->set($keyName, new \stdClass());实际上是什么,你会在任何真实的情景体验,因为PHP是无共享的d在每个请求上重建整个堆栈。功能测试套件模仿这种行为不会产生错误的结果。一个例子是具有ObjectCache的原则,因此您可以创建对象,而不是将它们保存到数据库,并且您的测试都会通过,因为它始终将对象从缓存中取出。

您可以用不同的方式解决这个问题:

创建一个真正的类,这是一个TestDouble和模仿,你会期望从实际API的结果。这实际上非常简单:您创建一个新的MyApiClientTestDouble,其签名与您的正常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的参数变量(在开发或测试的情况下为测试,在生产环境中为真实的API )。

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

+0

感谢您的回复。我希望有使用mockbuilder,而不是注入stub类的方式,因为这意味着它不是简单的改变上测试通过测试的基础上模拟的行为(例如,如果我想模拟返回404,而不是500,我需要另一个存根类,而不是使用更灵活的模拟器)。 – ChrisC 2013-03-12 10:34:42

我不知道你是否曾经发现如何解决你的问题。但这是我使用的解决方案。这对其他人发现这一点也很有帮助。

一个漫长的探索与嘲讽多个客户机请求之间的服务的问题后,我发现这个博客帖子:

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

lyrixx说说每个请求进行服务后内核shutsdown如何overrid无效时您尝试提出另一个请求。

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

此AppTestKernel扩展了AppKernel并仅应用一些处理程序来修改内核: 来自lyrixx blogpost的代码示例。

<?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 setter和应用模拟

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()); 
    } 
} 

本指南我有以下后,一些问题得到phpunittest一起启动新的AppTestKernel。

我发现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加载,你将能够嘲笑多个客户机请求之间的服务。

当你调用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))); 
} 

现在你可能要创建一个单独的方法,嘲笑新鲜的实例你,所以你不必复制你的代码。 ..

+2

发布带有csrf保护的表单时这不起作用,因为这些标记不匹配 – Emilie 2015-11-16 15:03:16

基于由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(); 

     ..... 
+0

完美地工作,谢谢:)。 – 2016-01-21 10:58:02

做一个模拟:

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

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

上模拟对象替换服务名

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

我的问题是使用:

self::$kernel->getContainer();