2013-03-11 83 views
20

我有一個控制器我想創建功能測試。該控制器通過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()

回答

7

我以爲我會跳進來。 Chrisc,我想你想要的是在這裏:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

我同意你的一般方法,在作爲參數的服務容器配置這實在不是一個好辦法。整個想法是能夠在單個測試運行期間動態地嘲笑它。

+0

謝謝你。我沒有試過這個庫 - 我最終只用了一個由Symfony的DI確定的單個模擬類,這並不理想 - 但我肯定會考慮將來使用它。這似乎是最適合我原來的問題,所以我接受了這個答案。 – ChrisC 2014-06-11 18:40:37

2

行爲遇到前打電話$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

2

我不知道你是否曾經發現如何解決你的問題。但這是我使用的解決方案。這對其他人發現這一點也很有幫助。

一個漫長的探索與嘲諷多個客戶機請求之間的服務的問題後,我發現這個博客帖子:

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加載,你將能夠嘲笑多個客戶機請求之間的服務。

8

當你調用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

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

     ..... 
+0

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

0

做一個模擬:

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

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

上模擬對象替換服務名

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

我的問題是使用:

self::$kernel->getContainer();