2015-08-05 14 views
10

Prima di tutto, io sono consapevole delle docs stati:derisione Richiedi in laravel 5.1 per Test (reale) Unità

Note: You should not mock the Request facade. Instead, pass the input you desire into the HTTP helper methods such as call and post when running your test.

Ma questo tipo di test sono più simili integrazione o funzionale dal momento che anche sebbene tu stia testando un controller (il SUT), non lo stai disaccoppiando dalle sue dipendenze (Request e altri, più su questo più avanti).

Quindi quello che sto facendo, al fine di fare il corretto TDD ciclo, è beffardo il Repository, Response e Request (che ho problemi con).

mio aspetto test come questo:

public function test__it_shows_a_list_of_categories() { 
    $categories = []; 
    $this->repositoryMock->shouldReceive('getAll') 
     ->withNoArgs() 
     ->once() 
     ->andReturn($categories); 
    Response::shouldReceive('view') 
     ->once() 
     ->with('categories.admin.index') 
     ->andReturnSelf(); 
    Response::shouldReceive('with') 
     ->once() 
     ->with('categories', $categories) 
     ->andReturnSelf(); 

    $this->sut->index(); 

    // Assertions as mock expectations 
} 

Questo funziona perfettamente bene, e seguono il Arrange, legge, far valere stile.

Il problema è con Request, come il seguente:

public function test__it_stores_a_category() { 
    Redirect::shouldReceive('route') 
     ->once() 
     ->with('categories.admin.index') 
     ->andReturnSelf(); 

    Request::shouldReceive('only') 
     ->once() 
     ->with('name') 
     ->andReturn(['name' => 'foo']); 

    $this->repositoryMock->shouldReceive('create') 
     ->once() 
     ->with(['name' => 'foo']); 

    // Laravel facades wont expose Mockery#getMock() so this is a hackz 
    // in order to pass mocked dependency to the controller's method 
    $this->sut->store(Request::getFacadeRoot()); 

    // Assertions as mock expectations 
} 

Come potete vedere ho preso in giro Request::only('name') chiamata. Ma quando ho eseguito $ phpunit ottengo il seguente errore:

BadMethodCallException: Method Mockery_3_Illuminate_Http_Request::setUserResolver() does not exist on this mock object 

Dal momento che non sto chiamando direttamente setUserResolver() dal mio controller, questo significa che viene chiamato direttamente dalla realizzazione di Request. Ma perché? Ho preso in giro la chiamata al metodo, non dovrebbe chiamare alcuna dipendenza.

Cosa sto facendo male qui, perché ricevo questo messaggio di errore?

PS: Come bonus, sto guardando nel modo sbagliato forzando TDD con Test unitari su framework Laravel, poiché la documentazione è orientata ai test di integrazione accoppiando l'interazione tra le dipendenze e SUT con $this->call()?

+0

imbattuto in questo oggi. Correlati: https://twitter.com/laravelphp/status/556568018864459776 – Ravan

risposta

3

Si incontrano sempre problemi nel tentativo di unità di controllo dei test correttamente. Raccomando l'accettazione testandole con qualcosa come Codeception. Utilizzando i test di accettazione è possibile garantire che i controller/le viste gestiscano i dati in modo appropriato.

+3

Sì, i controller sono come la "colla" della vostra applicazione. Combinano molti dei tuoi servizi in azioni. Il punto di test unitario è di testare le unità di codice in isolamento. I controller non sono pensati per essere una piccola unità, legano molte cose insieme. È un caso di utilizzo molto migliore per un test di accettazione o funzionale. –

+1

Il problema non riguarda solo i controller. Oggi ho provato un test funzionale per il tratto di repository personalizzato che invalida la paginazione dei risultati attraverso l'impaginatore di Laravel. Si è scoperto che il metodo paginate() di Laravel legge il numero di pagina direttamente da Request-> input(), quindi devo prenderlo in giro in qualche modo per restituire il numero corretto della pagina, come richiesto dal mio test. – JustAMartin

+0

@JustAMartin Mi sono imbattuto nella stessa identica situazione oggi, il mio repository 'getPageOfDealers (Request $ request)' riceve un oggetto Request, quindi filtra e impagina il risultato di conseguenza. Ma per testare questo devo prendere in giro la richiesta. – UniFreak

7

L'unità che verifica un controller quando si utilizza Laravel non sembra una grande idea. Non mi preoccuperei dei singoli metodi che vengono chiamati sulla richiesta, sulla risposta e forse anche sulle classi del repository, dato il contesto di un controller.

Inoltre, l'unità che esegue il test di un controller appartenente a un framework perché si desidera disaccoppiare il test sut dalle sue dipendenze non ha senso, in quanto è possibile utilizzare quel controller solo in quel framework con quelle dipendenze fornite.

Poiché la richiesta, la risposta e le altre classi sono state completamente testate (tramite la classe Symfony sottostante, o dallo stesso Laravel), come sviluppatore mi preoccuperei solo di testare il codice che possiedo.

Scriverei un test di accettazione.

<?php 

use App\User; 
use App\Page; 
use App\Template; 
use App\PageType; 
use Illuminate\Foundation\Testing\WithoutMiddleware; 
use Illuminate\Foundation\Testing\DatabaseMigrations; 
use Illuminate\Foundation\Testing\DatabaseTransactions; 

class CategoryControllerTest extends TestCase 
{ 
    use DatabaseTransactions; 

    /** @test */ 
    public function test__it_shows_a_paginated_list_of_categories() 
    { 
     // Arrange 
     $categories = factory(Category::class, 30)->create(); 

     // Act 
     $this->visit('/categories') 

     // Assert 
      ->see('Total categories: 30') 
      // Additional assertions to verify the right categories can be seen may be a useful additional test 
      ->seePageIs('/categories') 
      ->click('Next') 
      ->seePageIs('/categories?page=2') 
      ->click('Previous') 
      ->seePageIs('/categories?page=1'); 
    } 

} 

Poiché questo test utilizza il DatabaseTransactions tratto, è facile eseguire la parte del processo, che quasi ti permette di leggere questo come un test di pseudo-unità di organizzare (ma questo è un piccolo sforzo di immaginazione).

Soprattutto, questo test verifica che le mie aspettative siano soddisfatte. Il mio test si chiama test_it_shows_a_paginated_list_of_categories e la mia versione del test fa esattamente questo. Sento che l'unit test route afferma solo che sono stati chiamati un sacco di metodi, ma non verifica mai che sto visualizzando un elenco delle categorie date su una pagina.

+0

Il mio caso d'uso personale è che ho creato una funzione di supporto che utilizza la funzione request() -> root() per determinare un risultato. per esempio. quando l'URL è questo, fallo. Quindi, voglio testare la mia funzione helper deridendo request() -> root() per restituire valori specifici ... ma non posso. Non c'è altro modo per deridere in modo appropriato la facciata della richiesta()? – Maccath

+1

Penso che se stai prendendo in giro la facciata, c'è un'opportunità per il refactoring. Per la tua funzione di supporto, mi aspetterei che accetti un parametro stringa '' '$ url'''. In questo modo puoi facilmente testare la tua funzione di supporto usando qualsiasi parametro tu voglia passare. – Amo

0

Ho tentato di deridere anche la richiesta per i miei test senza successo. Ecco come ho test se l'oggetto viene salvato:

public function test__it_stores_a_category() { 
    $this->action(
      'POST', 
      '[email protected]', 
      [], 
      [ 
       'name' => 'foo', 
      ] 
     ); 

    $this->assertRedirectedTo('categories/admin/index'); 

    $this->seeInDatabase('categories', ['name' => 'foo']); 
} 

spero che sia di qualche aiuto