2013-03-11 16 views
20

Ho un controller per cui vorrei creare test funzionali. Questo controller esegue richieste HTTP a un'API esterna tramite una classe MyApiClient. Ho bisogno di prendere in giro questa classe MyApiClient, in modo da poter verificare come il mio controller risponde per le risposte date (ad esempio cosa farà se la classe MyApiClient restituisce una risposta 500).Test funzionali di symfony 2 con servizi di simulazione

Non ho problemi a creare una versione derisoria della classe MyApiClient tramite lo standard PHPUnit mockbuilder: Il problema che sto riscontrando è che il mio controller utilizza questo oggetto per più di una richiesta.

Attualmente sto facendo il seguente nel mio 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; 
    } 
} 

Sembra come se il contenitore è in fase di ricostruzione quando la seconda richiesta è fatta, causando la MyApiClient creare un'istanza di nuovo. La classe MyApiClient è configurata per essere un servizio tramite un'annotazione (utilizzando il pacchetto aggiuntivo JMS DI) e immessa in una proprietà del controllore tramite un'annotazione.

Mi piacerebbe dividere ogni richiesta nel suo test per risolvere il problema se possibile, ma sfortunatamente non posso: ho bisogno di fare una richiesta al controller tramite un'azione GET e poi POST il modulo porta indietro. Mi piacerebbe farlo per due motivi:

1) Il modulo utilizza la protezione CSRF, quindi se invio POST direttamente al modulo senza utilizzare il crawler per inviarlo, il modulo non supera il controllo CSRF.

2) Verificare che il modulo generi la richiesta POST corretta quando viene inviato è un bonus.

Qualcuno ha qualche suggerimento su come farlo?

EDIT:

Questo può essere espressa nella seguente test di unità che non dipende da alcuna del mio codice, quindi potrebbe essere più chiaro:

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

Questo test ha esito negativo, anche se chiama $client->getContainer()->set($keyName, new \stdClass()); immediatamente prima della seconda chiamata a request()

risposta

7

Ho pensato di saltare qui. Chrisc, penso che quello che vuoi è qui:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

Sono d'accordo con il vostro approccio generale, la configurazione di questo nel contenitore di servizio come parametro non è davvero un buon approccio. L'idea è di essere in grado di deriderlo dinamicamente durante le singole prove di test.

+0

Grazie per questo. Non ho provato questa libreria - ho finito con l'avere una sola classe di simulazione che è determinata dal DI di Symfony, che non è l'ideale - ma sicuramente cercherò di usarlo in futuro. Questo sembra essere il più adatto al mio problema originale, quindi accetto questa risposta. – ChrisC

2

Il comportamento che si sta verificando è in realtà ciò che si sperimenterebbe in qualsiasi scenario reale, in quanto PHP non condivide nulla d ricostruisce l'intero stack su ogni richiesta. La suite di test funzionali imita questo comportamento per non generare risultati errati. Un esempio potrebbe essere la dottrina, che ha un ObjectCache, in modo da poter creare oggetti, non salvarli nel database e tutti i test passeranno perché rimuovono gli oggetti dalla cache tutto il tempo.

È possibile risolvere questo problema in vari modi:

creare una classe vera e propria che è un test double ed emula i risultati che ci si aspetta dal reale API. Questo è in realtà molto semplice: puoi creare un nuovo MyApiClientTestDouble con la stessa firma del tuo normale MyApiClient e cambiare semplicemente i corpi del metodo dove necessario.

Nella tua service.yml, è bene potrebbe avere questo:

parameters: 
    myApiClientClass: Namespace\Of\MyApiClient 

service: 
    myApiClient: 
    class: %myApiClientClass% 

Se questo è il caso, si può facilmente sovrascrivere quale classe viene preso aggiungendo quanto segue al config_test.yml:

parameters: 
    myApiClientClass: Namespace\Of\MyApiClientTestDouble 

Ora il contenitore di servizio utilizzerà TestDouble durante il test. Se entrambe le classi hanno la stessa firma, non serve più nulla. Non so se o come funziona con il pacchetto DI Extras. ma immagino ci sia un modo.

Oppure è possibile creare un ApiDouble, implementando un'API "reale" che si comporta allo stesso modo dell'API esterna ma restituisce i dati di test. Dovresti quindi rendere l'URI della tua API gestito dal contenitore del servizio (ad es. Setter injection) e creare una variabile di parametri che punta all'API corretta (quella di prova in caso di dev o test e quella reale in caso di ambiente di produzione).

Il terzo modo è un po 'hacky, ma è sempre possibile effettuare un metodo privato all'interno dei test request che prima imposta il contenitore nel modo giusto e quindi chiama il client per effettuare la richiesta.

+0

Grazie per la risposta. Speravo che ci fosse un modo per usare il mockbuilder, piuttosto che iniettare una classe stub, in quanto ciò significa che non è così semplice cambiare il comportamento del mock su base test-test (es. Se volessi simulare il ritorno di un 404, piuttosto che un 500, avrei bisogno di un'altra classe stub, piuttosto che usare il mockbuilder più flessibile). – ChrisC

2

Non so se hai mai scoperto come risolvere il tuo problema. Ma ecco la soluzione che ho usato. Questo è positivo anche per le altre persone che lo trovano.

Dopo una lunga ricerca per il problema con beffarda un servizio tra più richieste del client ho trovato questo post del blog:

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

lyrixx parlare di come la shutsdown kernel dopo ogni richiesta di rendere il servizio overrid valida quando tu provi a fare un'altra richiesta.

Per risolvere questo problema crea un AppTestKernel utilizzato solo per i test di funzione.

Questo AppTestKernel estende l'AppKernel e applica solo alcuni gestori per modificare il kernel: Esempi di codice dal blogpost di 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(); 
    } 
} 

Quando poi necessario eseguire l'override di un servizio nel test si chiama il setter sul testAppKernel e applica il 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()); 
    } 
} 

Dopo aver seguito questa guida ho avuto qualche difficoltà a trovare il phpunittest per l'avvio con il nuovo AppTestKernel.

Ho scoperto che symfonys WebTestCase (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php) Prende il primo file AppKernel che trova. Quindi un modo per uscire da questo è quello di cambiare il nome sulla AppTestKernel a venire prima AppKernel o per sovrascrivere il metodo di prendere il TestKernel Invece

Here i overrride il getKernelClass nella WebTestCase a cercare un * 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; 
} 

Dopo questo i test verranno caricati con il nuovo AppTestKernel e si sarà in grado di prendere in giro i servizi tra più richieste dei client.

8

Quando si chiama self::createClient(), si ottiene un'istanza avviata del kernel Symfony2. Ciò significa che tutta la configurazione viene analizzata e caricata. Quando invii una richiesta, lasci che il sistema faccia il suo lavoro per la prima volta, giusto?

Dopo la prima richiesta, è possibile controllare cosa è andato avanti e, pertanto, il kernel è in uno stato, in cui viene inviata la richiesta, ma è ancora in esecuzione.

Se ora si esegue una seconda richiesta, l'architettura Web richiede che il kernel si riavvii perché ha già eseguito una richiesta. Questo riavvio, nel codice, viene eseguito quando si esegue una richiesta per la seconda volta.

Se si desidera avviare il kernel e modificarlo prima che la richiesta venga inviata ad esso (come si desidera), è necessario chiudere la vecchia istanza del kernel e avviarne una nuova.

È possibile farlo semplicemente ripetendo self::createClient(). Ora devi di nuovo applicare la tua finta, come hai fatto la prima volta.

Questo è il codice modificato del secondo esempio:

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

Ora è possibile creare un metodo separato, che prende in giro l'istanza fresco per voi, in modo da non dovete copiare il codice. ..

+2

Questo non funzionerà quando si invia un modulo con protezione csrf perché i token non corrispondono a – Emilie

2

In base alla risposta di Mibsen, è anche possibile impostarlo in modo simile estendendo il WebTestCase e sovrascrivendo il metodo createClient. Qualcosa in questo senso:

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

Poi nel test che si sarebbe fare qualcosa di simile:

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

Funziona PERFETTAMENTE, grazie :). –

0

Fai un finto:

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

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

Sostituire service_name sul mock-oggetto:

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

Il mio problema era quello di utilizzare:

self::$kernel->getContainer();