Testen als een baas in Laravel modellen

Als u hoopt te leren waarom tests gunstig zijn, is dit niet het artikel voor u. In de loop van deze tutorial ga ik ervan uit dat je de voordelen al begrijpt en hoop je te leren hoe je het best je tests kunt schrijven en organiseren in Laravel 4.

Versie 4 van Laravel biedt serieuze verbeteringen ten opzichte van testen in vergelijking met de vorige versie. Dit is het eerste artikel van een serie dat gaat over het schrijven van tests voor Laravel 4-toepassingen. We beginnen de serie door modeltesten te bespreken.


Opstelling

In-memory database

Tenzij u raw-query's uitvoert in uw database, staat Laravel uw applicatie toe om database-agnostisch te blijven. Met een eenvoudige wijziging van stuurprogramma kan uw toepassing nu met andere DBMS werken (MySQL, PostgreSQL, SQLite, enz.). Van de standaardopties biedt SQLite een eigenaardige, maar zeer nuttige functie: databases in het geheugen.

Met Sqlite kunnen we de databaseverbinding instellen :geheugen:, die onze testen drastisch zal versnellen, vanwege de database die niet op de harde schijf aanwezig is. Bovendien zal de productie / ontwikkelingsdatabase nooit worden gevuld met overgebleven testgegevens, omdat de verbinding, :geheugen:, begint altijd met een lege database.

Kort gezegd: een database in het geheugen zorgt voor snelle en schone tests.

Binnen de app / config / testen map, maak een nieuw bestand met de naam database.php, en vul het met de volgende inhoud:

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

Het feit dat database.php wordt in de configuratie geplaatst testing map betekent dat deze instellingen alleen worden gebruikt in een testomgeving (die Laravel automatisch instelt). Als zodanig, wanneer uw toepassing normaal wordt geopend, zal de in-memory database niet worden gebruikt.

Voordat u tests uitvoert

Omdat de in-memory database altijd leeg is wanneer een verbinding tot stand is gebracht, is het belangrijk om trekken de database vóór elke test. Om dit te doen, open app / testen / TestCase.php en voeg de volgende methode toe aan het einde van de klas:

/ ** * Migreert de database en stelt de mailer in op 'doen alsof'. * Hierdoor zullen de tests snel worden uitgevoerd. * * / private function prepareForTests () Artisan :: call ('migrate'); Mail :: pretenderen (true); 

Merk op opstelling() methode wordt vóór elke test door PHPUnit uitgevoerd.

Deze methode bereidt de database voor en verandert de status van Laravel's verzender les naar doen alsof. Op deze manier zal de Mailer geen echte e-mail verzenden tijdens het uitvoeren van tests. In plaats daarvan zal het de "verzonden" berichten loggen.

Beëindigen app / testen / TestCase.php, telefoontje prepareForTests () binnen de PHPUnit opstelling() methode, die vóór elke test zal worden uitgevoerd.

Vergeet het ouder :: Instellingen (), omdat we de methode van de ouderklasse overschrijven.

/ ** * Standaardvoorbereiding voor elke test * * / openbare functiesetUp () parent :: setUp (); // Vergeet dit niet! $ This-> prepareForTests (); 

Op dit punt, app / testen / TestCase.php zou moeten lijken op de volgende code. Onthoudt dat createApplication wordt automatisch gemaakt door Laravel. U hoeft zich er geen zorgen over te maken.

// app / tests / TestCase.php prepareForTests ();  / ** * Creëert de applicatie. * * @return Symfony \ Component \ HttpKernel \ HttpKernelInterface * / public function createApplication () $ unitTesting = true; $ testEnvironment = 'testen'; return vereist __DIR __. '/ ... / ... /start.php';  / ** * Migreert de database en stelt de mailer in op 'doen alsof'. * Hierdoor zullen de tests snel worden uitgevoerd. * / private function prepareForTests () Artisan :: call ('migrate'); Mail :: pretenderen (true); 

Nu, om onze tests te schrijven, eenvoudig uitbreiden testcase, en de database wordt vóór elke test geïnitialiseerd en gemigreerd.


De testen

Het is correct om te zeggen dat we in dit artikel de. Niet zullen volgen TDD werkwijze. Het probleem is hier didactisch, met als doel om aan te tonen hoe de tests kunnen worden geschreven. Om deze reden heb ik ervoor gekozen eerst de betreffende modellen te onthullen, en daarna hun bijbehorende tests. Ik geloof dat dit een betere manier is om deze tutorial te illustreren.

De context van deze demotoepassing is een eenvoudige blog / CMS met gebruikers (authenticatie), berichten en statische pagina's (die in het menu worden weergegeven).

Plaats het model

Let op: het model breidt de klas uit, Ardent, in plaats van Welsprekend. Ardent is een pakket dat zorgt voor eenvoudige validatie, na het opslaan van het model (zie de $ regels eigendom).

Vervolgens hebben we de openbare statische $ fabriek array, die gebruikmaakt van het FactoryMuff-pakket, om te helpen bij het maken van objecten tijdens het testen.

Beide Ardentx en FactoryMuff zijn beschikbaar via Packagist en Composer.

In onze Post model, we hebben een relatie met de Gebruiker model, door de magie schrijver methode.

Ten slotte hebben we een eenvoudige methode die de datum retourneert, opgemaakt als "dag maand jaar".

// app / models / Post.php  'required', // Post tittle 'slug' => 'required | alpha_dash', // Post URL 'content' => 'required', // Inhoud plaatsen (Markdown) 'author_id' => 'verplicht | numeriek', // Auteur id); / ** * Array gebruikt door FactoryMuff om testobjecten te maken * / public static $ factory = array ('title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id '=>' factory | User ', // is het ID van een bestaande gebruiker.); / ** * Behoort tot gebruiker * / public function author () return $ this-> belongTo ('User', 'author_id');  / ** * Ontvang opgemaakte postdatum * * @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'); 

Na tests

Om dingen georganiseerd te houden, heb ik de klas geplaatst met de Post modelproeven in app / testen / modellen / PostTest.php. We zullen alle tests doorlopen, een sectie per keer.

// app / tests / models / PostTest.php  

We verlengen de testcase klasse, wat een vereiste is voor PHPUnit-testen in Laravel. Vergeet ook ons ​​niet prepareTests methode die voor elke test wordt uitgevoerd.

 public function test_relation_with_author () // Instantiëren, invullen met waarden, opslaan en terugzenden $ post = FactoryMuff :: create ('Post'); // Dankzij FactoryMuff heeft deze $ post een auteur $ this-> assertEquals ($ post-> author_id, $ post-> author-> id); 

Deze test is een "optionele" test. We testen dat de relatie "Post hoort bij GebruikerHet doel is hier vooral om de functionaliteit van FactoryMuff te demonstreren.

Zodra de Post klasse hebben de $ fabriek statische array met 'author_id' => 'factory | Gebruiker' (let op de broncode van het model, hierboven weergegeven) de FactoryMuff maakt een nieuw Gebruiker vult zijn attributen, slaat op in de database en retourneert uiteindelijk zijn id naar de author_id attribuut in de Post.

Om dit mogelijk te maken, is de Gebruiker model moet een hebben $ fabriek array die ook de velden beschrijft.

Merk op hoe je toegang hebt tot de Gebruiker relatie door $ Post-> auteur. Als voorbeeld hebben we toegang tot de $ Post-> Auteur-> gebruikersnaam, of een ander bestaand gebruikerskenmerk.

Het FactoryMuff-pakket maakt een snelle instantiatie mogelijk van consistente objecten met het oog op testen, terwijl de benodigde relaties worden gerespecteerd en geïnstantieerd. In dit geval, wanneer we een maken Post met FactoryMuff :: te maken ( 'Post') de Gebruiker zal ook worden voorbereid en beschikbaar gemaakt.

 public function test_posted_at () // Instantiëren, invullen met waarden, opslaan en terugzenden $ post = FactoryMuff :: create ('Post'); // Reguliere expressie die d / m / Y-patroon vertegenwoordigt $ expected = '/ \ d 2 \ / \ d 2 \ / \ d 4 /'; // Waar als preg_match het patroon vindt $ matches = (preg_match ($ expected, $ post-> postedAt ()))? waar onwaar; $ this-> assertTrue ($ matches); 

Om te eindigen, bepalen we of de string geretourneerd door de postedAt () methode volgt het formaat "dag / maand / jaar". Voor een dergelijke verificatie wordt een reguliere expressie gebruikt om te testen of het patroon \ D 2 \ / \ d 2 \ / \ d 4 ("2 cijfers" + "balk" + "2 cijfers" + "balk" + "4 cijfers") is gevonden.

Als alternatief kunnen we de assertRegExp-matcher van PHPUnit gebruiken.

Op dit punt, de app / testen / modellen / PostTest.php bestand is als volgt:

// app / tests / models / PostTest.php assertEquals ($ post-> author_id, $ post-> author-> id);  openbare functie test_posted_at () // Onmiddellijk, vul met waarden, sla op en retourneer $ post = FactoryMuff :: create ('Post'); // Reguliere expressie die d / m / Y-patroon vertegenwoordigt $ expected = '/ \ d 2 \ / \ d 2 \ / \ d 4 /'; // Waar als preg_match het patroon vindt $ matches = (preg_match ($ expected, $ post-> postedAt ()))? waar onwaar; $ this-> assertTrue ($ matches); 

PS: ik heb ervoor gekozen de naam van de tests in CamelCase niet te schrijven omwille van de leesbaarheid. PSR-1 vergeef me, maar testRelationWithAuthor is niet zo leesbaar als ik persoonlijk zou willen. Je bent natuurlijk vrij om de stijl te gebruiken die je het liefst hebt.

Paginamodel

Ons CMS heeft een model nodig om statische pagina's weer te geven. Dit model is als volgt geïmplementeerd:

 'vereist', // Paginatitel 'slug' => 'vereist | alpha_dash', // Slug (url) 'content' => 'vereist', // Inhoud (markdown) 'author_id' => 'verplicht | numeriek' , // Auteur id); / ** * Array gebruikt door FactoryMuff * / public static $ factory = array ('title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id' => ' fabriek | Gebruiker ', // is het ID van een bestaande gebruiker.); / ** * Behoort tot gebruiker * / public function author () return $ this-> belongTo ('User', 'author_id');  / ** * Herstelt het menu met cache * * @return string Html voor paginalinks. * / public static function renderMenu () $ pages = Cache :: rememberForever ('pages_for_menu', function () return Page :: select (array ('title', 'slug')) -> get () -> toArray ();); $ result = "; foreach ($ pagina's als $ pagina) $ result. = HTML :: actie ('PagesController @ show', $ page ['title'], ['slug' => $ page ['slug'] ]). ' | '; return $ result; / ** * Cache vergeten wanneer opgeslagen * / public function afterSave ($ succes) if ($ succes) Cache :: forget (' pages_for_menu '); / ** * Cache vergeten wanneer delete * / public function delete () parent :: delete (); Cache :: forget ('pages_for_menu');

We kunnen waarnemen dat de statische methode, renderMenu (), rendert een aantal links voor alle bestaande pagina's. Deze waarde wordt opgeslagen in de cachesleutel, 'Pages_for_menu'. Op deze manier, in toekomstige oproepen naar renderMenu (), het is niet nodig om de echte database te raken. Dit kan de prestaties van onze applicatie aanzienlijk verbeteren.

Echter, als een Pagina wordt opgeslagen of verwijderd (afterSave () en wissen () methoden), wordt de waarde van de cache gewist, waardoor de renderMenu () om de nieuwe staat van de database weer te geven. Dus als de naam van een pagina is gewijzigd of als deze is verwijderd, is de sleutel 'pages_for_menu' wordt uit de cache gewist. (Cache :: vergeten ( 'pages_for_menu');)

OPMERKING: de methode, afterSave (), is beschikbaar via het Ardent-pakket. Anders zou het moeten worden geïmplementeerd opslaan() methode om de cache te reinigen en te bellen ouder :: save ();

Paginatests

In: app / testen / modellen / PageTest.php, we zullen de volgende tests schrijven:

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

Nogmaals, we hebben een "optionele" test om de relatie te bevestigen. Zoals relaties de verantwoordelijkheid zijn van Verlichten \ Database \ Welsprekend, die al door de eigen tests van Laravel wordt gedekt, hoeven we geen nieuwe test te schrijven om te bevestigen dat deze code werkt zoals verwacht.

 openbare functie test_render_menu () $ pages = array (); voor ($ 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));  // Controleer of cache is geschreven $ this-> assertNotNull (Cache :: get ('pages_for_menu')); 

Dit is een van de belangrijkste tests voor de Pagina model. Eerst worden vier pagina's gemaakt in de voor lus. Hierna is het resultaat van de renderMenu () oproep wordt opgeslagen in de $ result variabel. Deze variabele moet een HTML-tekenreeks bevatten met koppelingen naar de bestaande pagina's.

De foreach loop controleert of de slug (url) van elke pagina aanwezig is $ result. Dit is voldoende, omdat het exacte formaat van de HTML niet relevant is voor onze behoeften.

Ten slotte bepalen we of de cachesleutel, pages_for_menu, heeft iets opgeslagen. Met andere woorden, deed de renderMenu () call heeft daadwerkelijk wat waarde in de cache opgeslagen?

 public function test_clear_cache_after_save () // Een testwaarde wordt opgeslagen in cache Cache :: put ('pages_for_menu', 'avalue', 5); // Dit zou de waarde in cache moeten opschonen $ page = FactoryMuff :: create ('Page'); $ This-> assertNull (Cache :: te krijgen ( 'pages_for_menu')); 

Deze test is bedoeld om te verifiëren of, bij het opslaan van een nieuwe Pagina, de cachesleutel 'Pages_for_menu' is geleegd. De FactoryMuff :: te maken ( 'Pagina'); uiteindelijk triggert de opslaan() methode, dus dat zou voor de sleutel moeten volstaan, 'Pages_for_menu', worden gewist.

 public function test_clear_cache_after_delete () $ page = FactoryMuff :: create ('Page'); // Een testwaarde wordt opgeslagen in de cache Cache :: put ('pages_for_menu', 'value', 5); // Dit zou de waarde in cache $ page-> delete () moeten opschonen; $ This-> assertNull (Cache :: te krijgen ( 'pages_for_menu')); 

Vergelijkbaar met de vorige test, deze bepaalt of de sleutel 'Pages_for_menu' is leeggemaakt na het verwijderen van a Pagina.

Jouw PageTest.php zou er zo uit moeten zien:

assertEquals ($ page-> author_id, $ page-> author-> id);  public function test_render_menu () $ pages = array (); voor ($ 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));  // Controleer of cache is geschreven $ this-> assertNotNull (Cache :: get ('pages_for_menu'));  public function test_clear_cache_after_save () // Een testwaarde wordt opgeslagen in cache Cache :: put ('pages_for_menu', 'avalue', 5); // Dit zou de waarde in cache moeten opschonen $ page = FactoryMuff :: create ('Page'); $ This-> assertNull (Cache :: te krijgen ( 'pages_for_menu'));  public function test_clear_cache_after_delete () $ page = FactoryMuff :: create ('Page'); // Een testwaarde wordt opgeslagen in de cache Cache :: put ('pages_for_menu', 'value', 5); // Dit zou de waarde in cache $ page-> delete () moeten opschonen; $ This-> assertNull (Cache :: te krijgen ( 'pages_for_menu')); 

Gebruikersmodel

Gerelateerd aan de eerder gepresenteerde modellen, hebben we nu de Gebruiker. Hier is de code voor dat model:

 'string', 'email' => 'email', 'wachtwoord' => '123123', 'password_confirmation' => '123123',); / ** * Heeft veel pagina's * / public function pages () return $ this-> hasMany ('Page', 'author_id');  / ** * Heeft veel posts * / public function posts () return $ this-> hasMany ('Post', 'author_id'); 

Dit model is afwezig van tests.

We kunnen vaststellen dat, met uitzondering van relaties (die nuttig kunnen zijn om te testen), hier geen methode wordt geïmplementeerd. Hoe zit het met authenticatie? Welnu, het gebruik van het Confide-pakket biedt al de implementatie en tests hiervoor.

De tests voor Zizaco \ Confide \ ConfideUser bevinden zich in ConfideUserTest.php.

Het is belangrijk om de verantwoordelijkheden van de klas te bepalen voordat u uw tests schrijft. De optie testen om "reset het wachtwoord" van a Gebruiker zou overbodig zijn. Dit komt omdat de juiste verantwoordelijkheid voor deze test aanwezig is Zizaco \ Confide \ ConfideUser; niet in Gebruiker.

Hetzelfde geldt voor gegevensvalidatietests. Omdat het pakket, Ardent, deze verantwoordelijkheid op zich neemt, zou het niet veel zin hebben om de functionaliteit opnieuw te testen.

Kortom: houd uw tests schoon en georganiseerd. Bepaal de eigen verantwoordelijkheid van elke klas en test alleen wat zijn verantwoordelijkheid is.


Conclusie

Het gebruik van een in-memory database is een goede gewoonte om snel tests tegen een database uit te voeren. Dankzij de hulp van sommige pakketten, zoals Ardent, FactoryMuff en Confide, kunt u de hoeveelheid code in uw modellen minimaliseren, terwijl de tests schoon en objectief blijven.

In het vervolg op dit artikel zullen we beoordelen controleur testen. Blijf kijken!

Ga aan de slag met Laravel 4 en laat ons je de essentie leren!