Testing Like a Boss in Laravel Models

Se speri di imparare perché i test sono vantaggiosi, questo non è l'articolo per te. Nel corso di questo tutorial, presumo che tu abbia già compreso i vantaggi e speri di imparare come scrivere e organizzare al meglio i tuoi test in Laravel 4.

La versione 4 di Laravel offre notevoli miglioramenti in relazione ai test, rispetto alla versione precedente. Questo è il primo articolo di una serie che tratterà come scrivere test per le applicazioni Laravel 4. Inizieremo la serie discutendo i test dei modelli.


Impostare

Database in memoria

A meno che non si eseguano query non elaborate sul proprio database, Laravel consente alla propria applicazione di rimanere indipendente dal database. Con una semplice modifica del driver, l'applicazione ora può funzionare con altri DBMS (MySQL, PostgreSQL, SQLite, ecc.). Tra le opzioni predefinite, SQLite offre una caratteristica particolare, ma molto utile: i database in memoria.

Con Sqlite, possiamo impostare la connessione del database a :memoria:, che velocizzerà drasticamente i nostri test, a causa del database non esistente sul disco rigido. Inoltre, il database di produzione / sviluppo non verrà mai popolato con dati test rimanenti, poiché la connessione, :memoria:, inizia sempre con un database vuoto.

In breve: un database in memoria consente test rapidi e puliti.

All'interno del app / config / testing directory, creare un nuovo file, denominato database.php, e riempilo con il seguente contenuto:

// app / config / testing / database.php  'sqlite', 'connections' => array ('sqlite' => array ('driver' => 'sqlite', 'database' => ': memoria:', 'prefix' => "),));

Il fatto che database.php è posizionato all'interno della configurazione analisi directory significa che queste impostazioni saranno utilizzate solo in un ambiente di test (che Laravel imposta automaticamente). Pertanto, quando si accede normalmente alla propria applicazione, il database in memoria non verrà utilizzato.

Prima di eseguire i test

Poiché il database in memoria è sempre vuoto quando viene stabilita una connessione, è importante farlo migrare il database prima di ogni test. Per fare questo, apri app / test / TestCase.php e aggiungi il seguente metodo alla fine della classe:

/ ** * Migra il database e imposta il mailer su 'fingere'. * In questo modo i test verranno eseguiti rapidamente. * * / funzione privata prepareForTests () Artisan :: call ('migrate'); Mail :: finta (true); 

Notare la impostare() il metodo viene eseguito da PHPUnit prima di ogni test.

Questo metodo preparerà il database e cambierà lo stato di Laravel Mailer classe a fingere. In questo modo, il Mailer non invierà alcuna vera e-mail durante l'esecuzione dei test. Invece, registrerà i messaggi "inviati".

Per finalizzare app / test / TestCase.php, chiamata prepareForTests () all'interno di PHPUnit impostare() metodo, che verrà eseguito prima di ogni test.

Non dimenticare il parent :: setup (), poiché stiamo sovrascrivendo il metodo della classe genitore.

/ ** * Preparazione predefinita per ogni test * * / funzione pubblica setUp () parent :: setUp (); // Non dimenticarlo! $ This-> prepareForTests (); 

A questo punto, app / test / TestCase.php dovrebbe apparire come il seguente codice. Ricordatelo createApplication è creato automaticamente da Laravel. Non devi preoccuparti di questo.

// app / tests / TestCase.php prepareForTests ();  / ** * Crea l'applicazione. * * @return Symfony \ Component \ HttpKernel \ HttpKernelInterface * / public function createApplication () $ unitTesting = true; $ testEnvironment = 'testing'; return richiede __DIR __. '/ ... / ... /start.php';  / ** * Migra il database e imposta il mailer su 'fingere'. * In questo modo i test verranno eseguiti rapidamente. * / private function prepareForTests () Artisan :: call ('migrate'); Mail :: finta (true); 

Ora, per scrivere i nostri test, semplicemente estendilo TestCase, e il database verrà inizializzato e migrato prima di ogni test.


I test

È corretto dire che, in questo articolo, non seguiremo il TDD processi. Il problema qui è didattico, con l'obiettivo di dimostrare come i test possono essere scritti. Per questo motivo, ho scelto di rivelare prima i modelli in questione e poi i relativi test. Credo che questo sia un modo migliore per illustrare questo tutorial.

Il contesto di questa applicazione demo è un semplice blog / CMS, contenente utenti (autenticazione), post e pagine statiche (che sono mostrate nel menu).

Post modello

Si prega di notare che il modello estende la classe, Ardent, piuttosto che Eloquent. Ardent è un pacchetto che facilita la convalida, salvando il modello (vedi il $ regole proprietà).

Successivamente, abbiamo il fabbrica statica $ pubblica array, che sfrutta il pacchetto FactoryMuff per facilitare la creazione dell'oggetto durante il test.

Tutti e due Ardentx e FactoryMuff sono disponibili tramite Packagist e Composer.

Nel nostro Inviare modello, abbiamo una relazione con il Utente modello, attraverso la magia autore metodo.

Infine, abbiamo un metodo semplice che restituisce la data, formattata come "giorno mese Anno".

// app / models / Post.php  'required', // Post tittle 'slug' => 'required | alpha_dash', // Post Url 'content' => 'required', // Posta il contenuto (Markdown) 'author_id' => 'required | numerico', // id dell'autore); / ** * Matrice utilizzata da FactoryMuff per creare oggetti Test * / public static $ factory = array ('title' => 'string', 'slug' => 'stringa', 'contenuto' => 'testo', 'id_autore '=>' factory | User ', // sarà l'id di un utente esistente.); / ** * Appartiene all'utente * / public function author () return $ this-> belongsTo ('User', 'author_id');  / ** * Ottieni la data del post formattato * * @return string * / public function postedAt () $ date_obj = $ this-> created_at; if (is_string ($ this-> created_at)) $ date_obj = DateTime :: createFromFormat ('Y-m-d H: i: s', $ date_obj); return $ date_obj-> format ('d / m / Y'); 

Post test

Per mantenere le cose organizzate, ho messo la classe con il Inviare test modello in app / test / modelli / PostTest.php. Analizzeremo tutti i test, una sezione alla volta.

// app / tests / models / PostTest.php  

Estendiamo il TestCase classe, che è un requisito per il testing PHPUnit in Laravel. Inoltre, non dimenticare il nostro prepareTests metodo che verrà eseguito prima di ogni test.

 funzione pubblica test_relation_with_author () // Istanziare, riempire con valori, salvare e restituire $ post = FactoryMuff :: create ('Post'); // Grazie a FactoryMuff, questo $ post ha un autore $ this-> assertEquals ($ post-> author_id, $ post-> autore-> id); 

Questo test è uno "opzionale". Stiamo testando che la relazione "Inviare appartiene a Utente"Lo scopo qui è principalmente quello di dimostrare la funzionalità di FactoryMuff.

Una volta il Inviare la classe ha il $ fabbrica array statico contenente 'author_id' => 'factory | User' (notare il codice sorgente del modello, mostrato sopra) il FactoryMuff istanzia un nuovo Utente riempie i suoi attributi, salva nel database e infine restituisce il suo id al author_id attributo nel Inviare.

Perché ciò sia possibile, il Utente il modello deve avere un $ fabbrica array che descrive anche i suoi campi.

Si noti come è possibile accedere a Utente relazione attraverso $ Post-> autore. Ad esempio, possiamo accedere a $ Post-> auto-> nome utente, o qualsiasi altro attributo utente esistente.

Il pacchetto FactoryMuff consente una rapida istanziazione di oggetti coerenti ai fini del test, rispettando e istanziando le relazioni necessarie. In questo caso, quando creiamo a Inviare con FactoryMuff :: Create ( 'Post') il Utente sarà anche preparato e reso disponibile.

 funzione pubblica test_posted_at () // Istanziare, riempire con valori, salvare e restituire $ post = FactoryMuff :: create ('Post'); // Espressione regolare che rappresenta il pattern d / m / Y $ expected = '/ \ d 2 \ / \ d 2 \ / \ d 4 /'; // Vero se preg_match trova lo schema $ matches = (preg_match ($ expected, $ post-> postedAt ()))? vero falso; $ this-> assertTrue ($ matches); 

Per finire, determiniamo se la stringa restituita da postedAt () il metodo segue il formato "giorno / mese / anno". Per tale verifica, viene utilizzata un'espressione regolare per verificare se il modello \ D 2 \ / \ d 2 \ / \ d 4 ("2 numeri" + "bar" + "2 numeri" + "bar" + "4 numeri") è stato trovato.

In alternativa, potremmo usare il matcher assertRegExp di PHPUnit.

A questo punto, il app / test / modelli / PostTest.php il file è il seguente:

// app / tests / models / PostTest.php assertEquals ($ post-> author_id, $ post-> autore-> id);  public function test_posted_at () // Istanziare, riempire con valori, salvare e restituire $ post = FactoryMuff :: create ('Post'); // Espressione regolare che rappresenta il pattern d / m / Y $ expected = '/ \ d 2 \ / \ d 2 \ / \ d 4 /'; // Vero se preg_match trova lo schema $ matches = (preg_match ($ expected, $ post-> postedAt ()))? vero falso; $ this-> assertTrue ($ matches); 

PS: ho scelto di non scrivere il nome dei test in CamelCase per motivi di leggibilità. PSR-1 perdonami, ma testRelationWithAuthor non è leggibile come preferirei personalmente. Sei libero di usare lo stile che preferisci, ovviamente.

Modello di pagina

Il nostro CMS necessita di un modello per rappresentare pagine statiche. Questo modello è implementato come segue:

 'required', // Titolo Pagina 'slug' => 'richiesto | alpha_dash', // Slug (url) 'content' => 'required', // Content (markdown) 'author_id' => 'required | numerico' , // id dell'autore); / ** * Matrice utilizzata da FactoryMuff * / public static $ factory = array ('title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id' => ' factory | User ', // sarà l'id di un utente esistente.); / ** * Appartiene all'utente * / public function author () return $ this-> belongsTo ('User', 'author_id');  / ** * Esegue il rendering del menu utilizzando la cache * * @return string Html per i collegamenti di pagina. * / public static function renderMenu () $ pages = Cache :: rememberForever ('pages_for_menu', function () return Page :: select (array ('title', 'slug')) -> get () -> toArray ();); $ result = "; foreach ($ pages as $ page) $ result. = HTML :: action ('PagesController @ show', $ page ['title'], ['slug' => $ page ['slug'] ]). ' | '; return $ result; / ** * Dimentica la cache quando è salvato * / public function afterSave ($ success) if ($ success) Cache :: forget (' pages_for_menu '); / ** * Dimentica la cache quando cancellato * / public function delete () parent :: delete (); Cache :: forget ('pages_for_menu');

Possiamo osservare che il metodo statico, renderMenu (), esegue il rendering di un numero di collegamenti per tutte le pagine esistenti. Questo valore è salvato nella chiave di cache, 'Pages_for_menu'. In questo modo, in future chiamate a renderMenu (), non ci sarà bisogno di colpire il vero database. Questo può fornire miglioramenti significativi alle prestazioni della nostra applicazione.

Tuttavia, se a Pagina viene salvato o cancellato (AfterSave () e Elimina() metodi), il valore della cache verrà cancellato, causando il renderMenu () per riflettere il nuovo stato del database. Quindi, se il nome di una pagina viene modificato, o se viene eliminato, il chiave 'pages_for_menu' è cancellato dalla cache. (Cache :: dimenticare ( 'pages_for_menu');)

NOTA: il metodo, AfterSave (), è disponibile tramite il pacchetto Ardent. Altrimenti, sarebbe necessario implementare il salvare() metodo per pulire la cache e chiamare parent :: save ();

Test della pagina

Nel: app / test / modelli / PageTest.php, scriveremo i seguenti test:

assertEquals ($ page-> author_id, $ page-> author-> id); 

Ancora una volta, abbiamo un test "opzionale" per confermare la relazione. Poiché le relazioni sono responsabilità di Illuminare \ Database \ Eloquente, che è già coperto dagli stessi test di Laravel, non è necessario scrivere un altro test per confermare che questo codice funzioni come previsto.

 public function test_render_menu () $ pages = array (); per ($ i = 0; $ i < 4; $i++)  $pages[] = FactoryMuff::create('Page');  $result = Page::renderMenu(); foreach ($pages as $page)  // Check if each page slug(url) is present in the menu rendered. $this->assertGreaterThan (0, strpos ($ result, $ page-> slug));  // Controlla se la cache è stata scritta $ this-> assertNotNull (Cache :: get ('pages_for_menu')); 

Questo è uno dei test più importanti per il Pagina modello. Innanzitutto, vengono create quattro pagine nel per ciclo continuo. In seguito, il risultato del renderMenu () la chiamata è memorizzata nel $ result variabile. Questa variabile dovrebbe contenere una stringa HTML, contenente collegamenti alle pagine esistenti.

Il per ciascuno loop controlla se lo slug (url) di ogni pagina è presente $ result. Questo è sufficiente, dal momento che il formato esatto dell'HTML non è pertinente alle nostre esigenze.

Alla fine, determiniamo se la chiave di cache, pages_for_menu, ha qualcosa memorizzato In altre parole, ha fatto il renderMenu () la chiamata ha effettivamente salvato un certo valore nella cache?

 public function test_clear_cache_after_save () // Un valore di test viene salvato nella cache Cache :: put ('pages_for_menu', 'avalue', 5); // Questo dovrebbe pulire il valore nella cache $ page = FactoryMuff :: create ('Pagina'); $ This-> assertNull (Cache :: get ( 'pages_for_menu')); 

Questo test ha lo scopo di verificare se, quando si salva un nuovo Pagina, la chiave della cache 'Pages_for_menu' è svuotato. Il FactoryMuff :: create ( 'Pagina'); alla fine attiva il salvare() metodo, quindi dovrebbe essere sufficiente per la chiave, 'Pages_for_menu', essere chiarito.

 funzione pubblica test_clear_cache_after_delete () $ page = FactoryMuff :: create ('Pagina'); // Un valore di test viene salvato nella cache Cache :: put ('pages_for_menu', 'value', 5); // Questo dovrebbe pulire il valore nella cache $ page-> delete (); $ This-> assertNull (Cache :: get ( 'pages_for_menu')); 

Simile al test precedente, questo determina se la chiave 'Pages_for_menu' viene svuotato correttamente dopo aver cancellato a Pagina.

Il tuo PageTest.php dovrebbe apparire così:

assertEquals ($ page-> author_id, $ page-> author-> id);  public function test_render_menu () $ pages = array (); per ($ i = 0; $ i < 4; $i++)  $pages[] = FactoryMuff::create('Page');  $result = Page::renderMenu(); foreach ($pages as $page)  // Check if each page slug(url) is present in the menu rendered. $this->assertGreaterThan (0, strpos ($ result, $ page-> slug));  // Controlla se la cache è stata scritta $ this-> assertNotNull (Cache :: get ('pages_for_menu'));  public function test_clear_cache_after_save () // Un valore di test viene salvato nella cache Cache :: put ('pages_for_menu', 'avalue', 5); // Questo dovrebbe pulire il valore nella cache $ page = FactoryMuff :: create ('Pagina'); $ This-> assertNull (Cache :: get ( 'pages_for_menu'));  public function test_clear_cache_after_delete () $ page = FactoryMuff :: create ('Pagina'); // Un valore di test viene salvato nella cache Cache :: put ('pages_for_menu', 'value', 5); // Questo dovrebbe pulire il valore nella cache $ page-> delete (); $ This-> assertNull (Cache :: get ( 'pages_for_menu')); 

Modello utente

Relativo ai modelli precedentemente presentati, ora abbiamo il Utente. Ecco il codice per quel modello:

 'string', 'email' => 'email', 'password' => '123123', 'password_confirmation' => '123123',); / ** * Ha molte pagine * / public function pages () return $ this-> hasMany ('Page', 'author_id');  / ** * Ha molti post * / public function posts () return $ this-> hasMany ('Post', 'author_id'); 

Questo modello è assente nei test.

Possiamo osservare che, ad eccezione delle relazioni (che possono essere utili per testare), non c'è alcuna implementazione del metodo qui. Che dire dell'autenticazione? Bene, l'uso del pacchetto Confide fornisce già l'implementazione e i test per questo.

I test per Zizaco \ Confide \ ConfideUser si trovano in ConfideUserTest.php.

È importante determinare le responsabilità della classe prima di scrivere i test. Testare l'opzione a "resettare la password" di una Utente sarebbe ridondante. Questo perché la corretta responsabilità di questo test è all'interno Zizaco \ Confide \ ConfideUser; Non in Utente.

Lo stesso vale per i test di convalida dei dati. Dato che il pacchetto, Ardent, gestisce questa responsabilità, non avrebbe molto senso testare di nuovo la funzionalità.

In breve: mantieni i tuoi test puliti e organizzati. Determina la corretta responsabilità di ogni classe e verifica solo ciò che è strettamente di sua competenza.


Conclusione

L'uso di un database in memoria è una buona pratica per eseguire rapidamente test su un database. Grazie all'aiuto di alcuni pacchetti, come Ardent, FactoryMuff e Confide, puoi ridurre al minimo la quantità di codice nei tuoi modelli, mantenendo i test puliti e obiettivi.

Nel seguito di questo articolo, esamineremo controllore test. Rimanete sintonizzati!

Iniziamo ancora con Laravel 4, insegniamoci l'essenziale!