2013-03-11 14 views
20

Mam kontroler, dla którego chcę utworzyć testy funkcjonalne. Ten kontroler wysyła żądania HTTP do zewnętrznego interfejsu API za pośrednictwem klasy MyApiClient. Muszę wyśmiewać tę klasę MyApiClient, więc mogę przetestować, w jaki sposób mój kontroler odpowiada na podane odpowiedzi (np. Co zrobi, jeśli klasa MyApiClient zwróci odpowiedź 500).Testy funkcjonalne Symfony 2 z wyśmiewanymi usługami

Nie mam problemów z tworzeniem wyśmiewanej wersji klasy MyApiClient za pośrednictwem standardowego mockbuildera PHPUnit: Problem, który mam, polega na tym, że mój kontroler używa tego obiektu do więcej niż jednego żądania.

Jestem obecnie w następujący sposób w moim teście:

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

Wydaje się, że pojemnik jest przebudowywany, gdy drugi wniosek został złożony, powodując MyApiClient do ponownego wystąpienia. Klasa MyApiClient jest skonfigurowana jako usługa za pomocą adnotacji (przy użyciu pakietu JMS DI Extra) i wstrzyknięta do właściwości kontrolera za pomocą adnotacji.

Chciałbym podzielić każde żądanie na własny test, aby obejść ten problem, gdybym mógł, ale niestety nie mogę: muszę wysłać żądanie do kontrolera za pomocą akcji GET, a następnie POST formularza przynieść z powrotem. Chciałbym to zrobić z dwóch powodów:

1) Formularz korzysta z ochrony CSRF, więc jeśli po prostu POST bezpośrednio do formularza bez korzystania z przeszukiwacza, aby przesłać go, formularz nie przechodzi kontroli CSRF.

2) Testowanie, że formularz generuje prawidłowe żądanie POST po jego przesłaniu, jest bonusem.

Czy ktoś ma jakieś sugestie, jak to zrobić?

EDIT:

To może być wyrażona w następujący testów jednostkowych, które nie zależy od żadnego z mojego kodu, więc może być wyraźniejszy:

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

Ten test nie powiedzie się, nawet jeśli zadzwoń pod numer $client->getContainer()->set($keyName, new \stdClass()); bezpośrednio przed drugim połączeniem do request()

Odpowiedz

7

Myślałem, że tu wskoczę. Chrisc, myślę, że to, co chcesz jest tutaj:

https://github.com/PolishSymfonyCommunity/SymfonyMockerContainer

zgadzam się z ogólnym podejściem, to w konfiguracji kontenera serwisowego jako parametr nie jest to dobre podejście. Cała idea polega na tym, aby móc kpić z niej dynamicznie podczas poszczególnych przebiegów testowych.

+0

Dzięki za to. Nie próbowałem tej biblioteki - skończyło się na tym, że miałem tylko jedną próbną klasę, która jest określona przez DI DI Symfony, która nie jest idealna - ale na pewno przyjrzę się używaniu tego w przyszłości. To wydaje się najlepiej pasować do mojego pierwotnego problemu, więc akceptuję tę odpowiedź. – ChrisC

2

Zachowanie, którego doświadczasz, jest w rzeczywistości tym, czego doświadczasz w każdym rzeczywistym scenariuszu, ponieważ PHP nie ma nic wspólnego d przebudowuje cały stos przy każdym żądaniu. Zestaw testów funkcjonalnych imituje to zachowanie, aby nie generować złych wyników. Jednym z przykładów może być doktryna, która ma obiekt ObjectCache, aby można było tworzyć obiekty, a nie zapisywać ich w bazie danych, a wszystkie testy przebiegałyby pomyślnie, ponieważ cały czas zabiera obiekty z pamięci podręcznej.

Można rozwiązać ten problem na różne sposoby:

Tworzenie prawdziwą klasę, która jest TestDouble i emuluje wyniki, których można oczekiwać od prawdziwego API. W rzeczywistości jest to bardzo proste: Tworzysz nowy MyApiClientTestDouble z tym samym podpisem co zwykły MyApiClient i zmieniasz tylko ciała metod tam, gdzie jest to potrzebne.

W swojej service.yml, w porządku może mieć to:

parameters: 
    myApiClientClass: Namespace\Of\MyApiClient 

service: 
    myApiClient: 
    class: %myApiClientClass% 

Jeśli jest to przypadek, można łatwo zastąpić, która klasa jest podejmowana przez dodanie następujących do config_test.yml:

parameters: 
    myApiClientClass: Namespace\Of\MyApiClientTestDouble 

Teraz pojemnik serwisowy użyje Twojego TestDouble podczas testowania. Jeśli obie klasy mają ten sam podpis, nic więcej nie jest potrzebne. Nie wiem, czy i jak to działa z pakietem DI Extras. ale myślę, że istnieje sposób.

Możesz też utworzyć ApiDouble, implementując "prawdziwy" interfejs API, który zachowuje się w taki sam sposób jak zewnętrzny interfejs API, ale zwraca dane testowe. Następnie należy wykonać identyfikator URI interfejsu API przez kontener usługi (np. Zastrzyk ustawiający) i utworzyć zmienną parametrów, która wskazuje właściwy interfejs API (testowy w przypadku dev lub test, a prawdziwy w przypadku środowiska produkcyjnego).

Trzeci sposób jest nieco hacky, ale zawsze możesz zrobić prywatną metodę w swoich testach request, która najpierw ustawia pojemnik we właściwy sposób, a następnie dzwoni do klienta, aby to zrobić.

+0

Dzięki za odpowiedź. Miałem nadzieję, że istnieje sposób, aby użyć kreatora, zamiast wstrzykiwać klasę pośrednią, ponieważ oznacza to, że nie jest tak proste, aby zmienić zachowanie próbne na podstawie testu (np. Jeśli chciałem symulować zwrot 404, zamiast 500, potrzebowałbym kolejnej klasy pośredniczącej, zamiast używać bardziej elastycznego kreatora). – ChrisC

2

Nie wiem, czy kiedykolwiek dowiedziałeś się, jak rozwiązać problem. Ale tutaj jest rozwiązanie, którego używałem. Jest to również dobre dla innych osób.

Po długim poszukiwaniu problemu z wyśmianie usługę między wielu żądań klientów Znalazłem ten blogu:

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

lyrixx rozmawiać o tym, jak shutsdown jądra po każdym żądaniu podejmowania usługę overrid nieważne kiedy próbujesz złożyć kolejną prośbę.

Aby to naprawić, tworzy AppTestKernel używany tylko do testów funkcji.

Ten AppTestKernel rozszerza AppKernel i stosuje tylko niektóre moduły do ​​modyfikacji kodu: Przykłady kodów z blogu 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(); 
    } 
} 

Kiedy następnie trzeba zastąpić usługę w swoim teście wywołaniu ustawiająca na testAppKernel i stosuje 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()); 
    } 
} 

Po wykonaniu tej instrukcji miałem pewne problemy uzyskiwanie phpunittest do uruchamiania za pomocą nowy AppTestKernel.

Okazało się, że Symfony WebTestCase (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php) Staje się pierwszym znalezionym plikiem AppKernel. Tak więc jednym ze sposobów wyjścia z tego jest zmiana nazwy w AppTestKernel przed AppKernel lub zastąpienie metody, aby wziąć TestKernel Zamiast

Tutaj nadpisuję getKernelClass w WebTestCase, aby wyszukać * 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; 
} 

Po tym czasie testy zostaną załadowane za pomocą nowego AppTestKernel i będzie można kpić z usług między wieloma żądaniami klientów.

8

Po wywołaniu self::createClient(), masz startem wystąpienie jądra Symfony2. Oznacza to, że cała konfiguracja jest parsowana i ładowana. Wysyłając żądanie, pozwalasz systemowi wykonać to zadanie po raz pierwszy, prawda?

Po pierwsze żądanie, może chcesz sprawdzić, co dzieje się, a zatem jądro znajduje się w stanie, w którym wniosek został wysłany, ale to wciąż działa.

Jeśli teraz uruchomić drugi wniosek, web-architektura wymaga, że ​​jądro restartuje, bo już prowadził prośbę. Ten restart w twoim kodzie jest wykonywany, gdy wykonasz żądanie po raz drugi.

Jeśli chcesz uruchomić jądro i zmodyfikować go, zanim żądanie jest wysyłane do niego (jak chcesz), masz do zamykania starego jądra instancji i uruchomić świeżą.

Możesz to zrobić, ponownie uruchamiając self::createClient(). Teraz znowu musisz zastosować swoją próbę, tak jak to zrobiłeś po raz pierwszy.

Jest to zmodyfikowany kod z drugiego przykładu:

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

Teraz możesz utworzyć oddzielną metodę, która drwi świeże instancję dla ciebie, więc nie trzeba skopiować kod. ..

+2

To nie zadziała podczas wysyłania formularza z ochroną csrf, ponieważ tokeny nie będą pasować – Emilie

2

podstawie odpowiedź Mibsen można również skonfigurować to w podobny sposób, poprzez rozszerzenie WebTestCase i przesłanianie metody createClient. Coś wzdłuż tych linii:

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

Następnie w teście byś zrobił coś takiego:

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

Działa PERFEKCYJNIE, dziękuję :). –

0

Zrób makiety:

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

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

Wymień nazwa_usługi na makiety obiektu:

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

Mój problem polegał na użyciu:

self::$kernel->getContainer();