2013-03-11 6 views
20

Ich habe einen Controller ich für funktionale Tests erstellen möchte. Dieser Controller führt HTTP-Anforderungen an eine externe API über eine MyApiClient Klasse aus. Ich brauche diese MyApiClient Klasse zu verspotten, so kann ich testen, wie mein Controller für gegebene Antworten reagiert (z was wird es tun, wenn die MyApiClient Klasse eine 500-Antwort zurückgibt).Symfony 2 Funktionstests mit verspottete Dienstleistungen

Ich habe keine Probleme mit der Erstellung einer verspotteten Version der MyApiClient Klasse über den Standard-PHPUnit-Mockbuilder: Das Problem, das ich habe, ist mein Controller dieses Objekt für mehr als eine Anfrage zu verwenden.

Ich mache folgend zur Zeit in meinem Test:

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

Es scheint, als ob der Behälter, wenn die zweite Anforderung, die MyApiClient wieder instanziiert zu verursachen gemacht wird wieder aufgebaut werden. Die MyApiClient Klasse ausgebildet ist, einen Dienst über eine Anmerkung (den JMS DI Zusatz Bundle verwenden) und in eine Eigenschaft des Reglers über eine Anmerkung injiziert.

Ich würde jede Anfrage in einen eigenen Test teilen, um dies zu umgehen, wenn ich könnte, aber leider kann ich nicht: Ich muss eine Anfrage an den Controller über eine GET-Aktion machen und dann das Formular POST bringt zurück. Ich möchte dies aus zwei Gründen tun:

1) Das Formular verwendet CSRF-Schutz, also, wenn ich nur direkt an das Formular POST ohne den Crawler zu senden, das Formular schlägt die CSRF-Prüfung.

2) Prüfung, dass das Formular die richtige POST-Anforderung erzeugt, wenn es eingereicht wird ein Bonus.

Hat jemand irgendwelche Vorschläge, wie dies zu tun?

EDIT:

Dies kann in der folgenden Unit-Test ausgedrückt werden, die nicht auf irgendwelchen meiner Code abhängt, kann so klarer sein:

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

Dieser Test schlägt fehl, auch wenn ich Rufen Sie $client->getContainer()->set($keyName, new \stdClass()); direkt vor dem zweiten Anruf an request()

Antwort

7

Ich dachte, ich würde hier reinspringen. Chrisc, ich glaube, was Sie wollen, ist hier:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

ich mit Ihrem allgemeinen Ansatz zustimmen, dies als Parameter in den Service-Containern Konfiguration ist wirklich kein guter Ansatz. Die ganze Idee ist es, dies während einzelner Testläufe dynamisch nachzuahmen.

+0

Danke dafür. Ich habe diese Bibliothek nicht ausprobiert - am Ende habe ich nur eine einzige Scheinklasse, die von Symfony's DI bestimmt wird, was nicht ideal ist - aber ich werde definitiv darauf achten, dies in Zukunft zu verwenden. Dies scheint die beste Lösung für mein ursprüngliches Problem zu sein, also akzeptiere ich diese Antwort. – ChrisC

2

Das Verhalten, das Sie erleben, ist tatsächlich, was Sie in jedem realen Szenario erleben würden, wie PHP ist nichts teilen ein d baut den gesamten Stack bei jeder Anfrage neu auf. Die funktionale Testsuite imitiert dieses Verhalten, um keine falschen Ergebnisse zu erzielen. Ein Beispiel wäre die Doktrin mit einem ObjectCache. Sie könnten also Objekte erstellen, sie nicht in der Datenbank speichern und Ihre Tests würden alle bestehen, weil sie die Objekte ständig aus dem Cache nehmen.

Sie können dieses Problem auf unterschiedliche Weise lösen:

eine echte Klasse erstellen, die ein TestDouble und emuliert die Ergebnisse, die Sie aus der realen API erwarten. Dies ist eigentlich sehr einfach: Sie ein neues MyApiClientTestDouble mit der gleichen Signatur erstellen, wie Ihre normalen MyApiClient, und nur die Methodenrümpfe ändern, wo nötig.

In Ihrem service.yml, du in Ordnung könnte dies haben:

parameters: 
    myApiClientClass: Namespace\Of\MyApiClient 

service: 
    myApiClient: 
    class: %myApiClientClass% 

Wenn dies der Fall ist, können Sie einfach überschreiben, welche Klasse, indem Sie das folgende in der config_test genommen wird.yml:

Jetzt wird der Service-Container Ihre TestDouble beim Testen verwenden. Wenn beide Klassen die gleiche Signatur haben, ist nichts mehr erforderlich. Ich weiß nicht, ob und wie das mit dem DI-Extras-Bundle funktioniert. aber ich denke, es gibt einen Weg.

Oder Sie könnten einen ApiDouble erstellen, indem Sie eine "echte" API implementieren, die sich genauso verhält wie Ihre externe API, aber Testdaten zurückgibt. Sie würden dann den URI Ihrer API vom Servicebehälter bearbeiten lassen (zB Setter-Injection) und eine Parametervariable erstellen, die auf die richtige API zeigt (die Testversion bei Entwicklung oder Test und die reale bei der Produktionsumgebung)).

Der dritte Weg ist ein bisschen hacky, aber man kann immer eine private Methode in Ihrem Test request machen, die zuerst die Behälter in der richtigen Weise einrichtet und rufen dann die Client die Anforderung zu machen.

+0

Vielen Dank für Ihre Antwort. Ich hatte gehofft, dass es einen Weg gibt, den Mockbuilder zu verwenden, anstatt eine Stub-Klasse zu injizieren, da es nicht so einfach ist, das Verhalten des Mocks von Test zu Test zu ändern (zB wenn ich simulieren wollte 404, anstatt einer 500, würde ich eine andere Stub-Klasse benötigen, anstatt den flexibleren mockbuilder zu verwenden). – ChrisC

2

Ich weiß nicht, ob Sie jemals herausgefunden haben, wie Sie Ihr Problem beheben können. Aber hier ist die Lösung, die ich verwendet habe. Dies ist auch gut für andere Leute, die das finden.

Nach einer langen Suche nach dem Problem mit einem Dienst zwischen mehreren Client-Anfragen spöttischen ich diesen Blog-Eintrag gefunden:

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

lyrixx darüber reden, wie der Kernel shutsdown nach jeder Anforderung stellt den Dienst overrid ungültig, wenn Sie versuchen, eine andere Anfrage zu stellen.

Um dies zu beheben, erstellt er einen AppTestKernel, der nur für die Funktionstests verwendet wird.

Diese AppTestKernel erweitert die AppKernel und gelten nur einige Handler den Kernel Modifié: Codebeispiele aus 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(); 
    } 
} 

Wenn Sie dann einen Dienst in Ihrem Test außer Kraft setzen müssen Sie den Setzer auf dem testAppKernel nennen und wendet das Mock

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

dieses Handbuch Nach folgenden ich einige Probleme hatten die phpunittest immer mit der Inbetriebnahme der neue AppTestKernel.

Ich fand heraus, dass die symfonys WebTestCase (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php) die erste AppKernel-Datei findet, die es findet. So ein Ausweg aus dieser erhalten ist, den Namen auf dem AppTestKernel zu ändern zu kommen, bevor AppKernel oder die Methode zu überschreiben die TestKernel zu nehmen statt

Hier overrride ich das getKernelClass im WebTestCase für ein * TestKernel.php aussehen

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

nach diesen Ihren Tests mit dem neuen AppTestKernel laden und Sie in der Lage, Dienste zwischen mehreren Client-Anfragen zu verspotten.

8

Wenn Sie self::createClient() aufrufen, erhalten Sie eine gestartete Instanz des Symfony2-Kernels. Das heißt, die gesamte Konfiguration wird analysiert und geladen. Wenn Sie jetzt eine Anfrage senden, lassen Sie das System es zum ersten Mal machen, richtig?

Nach der ersten Anfrage möchten Sie vielleicht überprüfen, was passiert ist, und daher ist der Kernel in einem Zustand, in dem die Anfrage gesendet wird, aber es läuft noch.

Wenn Sie jetzt eine zweite Anfrage ausführen, erfordert die Web-Architektur, dass der Kernel neu gestartet wird, da er bereits eine Anfrage ausgeführt hat. Dieser Neustart in Ihrem Code wird ausgeführt, wenn Sie eine Anforderung zum zweiten Mal ausführen.

Wenn Sie den Kernel booten und ändern wollen, bevor die Anfrage an ihn gesendet wird (wie Sie wollen), müssen Sie die alte Kernel-Instanz herunterfahren und eine neue booten.

Sie können das tun, indem Sie einfach self::createClient() erneut ausführen. Jetzt müssen Sie wieder Ihren Spott anwenden, wie Sie es beim ersten Mal getan haben.

Dies ist der modifizierte Code Ihres zweiten Beispiels:

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

Nun möchten Sie können ein separates Verfahren schaffen, welche die neue Instanz für Dich spottet, so müssen Sie Ihren Code nicht kopieren. ..

+2

Dies funktioniert nicht, wenn Sie ein Formular mit csrf-Schutz posten, da die Token nicht übereinstimmen – Emilie

2

Basierend auf der Antwort von Mibsen können Sie dies auch auf ähnliche Weise einrichten, indem Sie das WebTestCase erweitern und die createClient-Methode überschreiben. Etwas in dieser Richtung:

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

Funktioniert PERFEKT, danke :). –

0

Machen Sie einen Mock:

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

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

ersetzen service_name auf Mock-Objekt:

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

Dann im Test, den Sie so etwas wie tun würde

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

Mein Problem war zu verwenden:

self::$kernel->getContainer();