Testa som en chef i Laravel Modeller

Om du hoppas att lära dig varför testerna är fördelaktiga, så är det inte artikeln för dig. Under denna handledning kommer jag att anta att du redan förstår fördelarna och hoppas kunna lära dig hur man bäst skriver och organiserar dina tester i Laravel 4.

Version 4 av Laravel erbjuder seriösa förbättringar i förhållande till testning, jämfört med tidigare utgåva. Detta är den första artikeln i en serie som kommer att omfatta hur man skriver tester för Laravel 4-applikationer. Vi börjar serien genom att diskutera modelltestning.


Inrätta

I minnesdatabasen

Om du inte kör raka frågor i din databas tillåter Laravel att din ansökan förbli databas agnostic. Med en enkel förarbyte kan din applikation nu fungera med andra DBMS (MySQL, PostgreSQL, SQLite, etc.). Bland standardalternativen erbjuder SQLite en märklig men ändå väldigt användbar funktion: databaser i minnet.

Med Sqlite kan vi ställa in databasanslutningen till :minne:, vilket kommer att drastiskt påskynda våra test, på grund av att databasen inte finns på hårddisken. Dessutom kommer databasen för produktion / utveckling aldrig att fyllas med överlämningsdata, eftersom anslutningen, :minne:, börjar alltid med en tom databas.

Kort sagt: En in-memory-databas möjliggör snabba och rena test.

Inom app / config / testning katalog, skapa en ny fil, namngiven database.php, och fyll i det med följande innehåll:

// app / config / test / database.php  'sqlite', 'connections' => array ('sqlite' => array ('drivrutin' => 'sqlite', 'database' => ': minne:', 'prefix' => ")

Faktumet att database.php placeras inom konfigurationen testning katalog betyder att dessa inställningar endast används i en testmiljö (vilken Laravel automatiskt ställer in). Som sådan, när din ansökan är tillgänglig normalt, kommer inte minnesdatabasen att användas.

Innan du kör test

Eftersom databasen i minnet alltid är tom när en anslutning görs är det viktigt att vandra databasen före varje test. För att göra detta, öppna app / test / TestCase.php och lägg till följande metod till slutet av klassen:

/ ** * Migrerar databasen och sätter brevaren till "låtsas". * Detta gör att testen körs snabbt. * * / privat funktion prepareForTests () Artisan :: call ('migrate'); Post :: låtsas (true); 

Notera inrätta() Metoden utförs av PHPUnit före varje test.

Denna metod kommer att förbereda databasen och ändra status för Laravel s mailer klass till låtsas. På så sätt skickar Mailer inte något riktigt e-postmeddelande när du kör test. Istället kommer det att logga in de "skickade" meddelandena.

Att slutföra app / test / TestCase.php, ring upp prepareForTests () inom PHPUnit inrätta() metod som kommer att utföras före varje test.

Glöm inte förälder :: SETUP (), som vi skriver över moderklassens metod.

/ ** * Standardförberedelse för varje test * * / public function setUp () förälder :: setUp (); // Glöm inte detta! $ This-> prepareForTests (); 

Vid denna punkt, app / test / TestCase.php ska se ut som följande kod. Kom ihåg det createApplication skapas automatiskt av Laravel. Du behöver inte oroa dig för det.

// app / test / TestCase.php prepareForTests ();  / ** * Skapar programmet. * * @return Symfony \ Component \ HttpKernel \ HttpKernelInterface * / public function createApplication () $ unitTesting = true; $ testEnvironment = 'testning'; retur kräver __DIR __. '/ ... / ... / start.php';  / ** * Migrerar databasen och sätter brevaren till "låtsas". * Detta gör att testen körs snabbt. * / privat funktion prepareForTests () Artisan :: call ('migrate'); Post :: låtsas (true); 

Nu, för att skriva våra test, utvidga vi bara Testfall, och databasen kommer att initieras och migreras före varje test.


Testen

Det är korrekt att säga att vi i denna artikel inte kommer att följa TDD bearbeta. Frågan här är didaktisk, med målet att visa hur testen kan skrivas. På grund av detta valde jag att avslöja modellerna i fråga först och sedan deras relaterade tester. Jag tror att det här är ett bättre sätt att illustrera denna handledning.

Sammanhanget med denna demo-applikation är en enkel blogg / CMS, innehållande användare (autentisering), inlägg och statiska sidor (som visas i menyn).

Postmodell

Observera att modellen utökar klassen Ardent snarare än Eloquent. Ardent är ett paket som gör det enkelt att validera, när man sparar modellen (se $ regler fast egendom).

Därefter har vi offentlig statisk $ fabrik array, som utnyttjar FactoryMuff-paketet, för att hjälpa till med att skapa objekt vid testning.

Både Ardentx och FactoryMuff finns tillgängliga via Packagist and Composer.

I vår Posta modell, vi har ett förhållande till Användare modell, genom magi författare metod.

Slutligen har vi en enkel metod som returnerar datumet formaterat som "dag månad år".

// app / modeller / Post.php  'Required', // Posttitel 'slug' => 'krävs' alpha_dash ', // Post Url' content '=>' krävs ', // Inlägg innehåll (Markdown)' author_id '=>' krävs | numeric ', // Författarens id); / ** * Array används av FactoryMuff för att skapa Testobjekt * / offentliga statiska $ factory = array ('title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id '=>' fabrik | Användare ', // Kommer vara ID för en existerande Användare.); / ** * Hör till användaren * / public function author () return $ this-> belongsTo ('User', 'author_id');  / ** * Få formaterad postdatum * * @returnsträng * / offentlig funktion postedAt () $ date_obj = $ this-> created_at; om (is_string ($ this-> created_at)) $ date_obj = DateTime :: createFromFormat ('Y-m-d H: i: s', $ date_obj); returnera $ date_obj-> format ('d / m / y'); 

Posttest

För att hålla saker organiserade har jag lagt klassen med Posta modell tester i app / tester / modeller / PostTest.php. Vi går igenom alla tester, en sektion åt gången.

// app / test / modeller / PostTest.php  

Vi förlänger Testfall klass, vilket är ett krav för PHPUnit testning i Laravel. Glöm inte heller vårt prepareTests metod som löper före varje test.

 public function test_relation_with_author () // Instantiate, fyll med värden, spara och returnera $ post = FactoryMuff :: skapa ('Post'); // Tack vare FactoryMuff har denna $ post en författare $ this-> assertEquals ($ post-> author_id, $ post-> author-> id); 

Detta test är en "valfri" en. Vi testar att förhållandet "Posta tillhör Användare". Syftet här är mestadels att visa FactoryMuffs funktionalitet.

När Posta klassen har $ fabrik statisk matris innehållande 'author_id' => 'factory | User' (notera modellens källkod, visas ovan) FactoryMuff inställa en ny Användare fyller dess attribut, spara i databasen och äntligen returnera dess id till author_id attribut i Posta.

För att detta ska vara möjligt, Användare Modellen måste ha en $ fabrik array som beskriver dess fält också.

Lägg märke till hur du kan komma åt Användare relation genom $ Post-> författare. Som ett exempel kan vi komma åt $ Post-> author-> användarnamn, eller någon annan befintlig användarattribut.

FactoryMuff-paketet möjliggör snabb instansering av konsekventa föremål för att testa, samtidigt som man respekterar och instanserar eventuella nödvändiga relationer. I det här fallet när vi skapar en Posta med FactoryMuff :: skapa (Post) de Användare kommer också att förberedas och ställas till förfogande.

 allmän funktion test_posted_at () // Instantiate, fyll med värden, spara och returnera $ post = FactoryMuff :: skapa ('Post'); // Regular expression som representerar d / m / Y mönster $ expected = '/ \ d 2 \ / \ d 2 \ / \ d 4 /'; // True om preg_match hittar mönstret $ matches = (preg_match ($ expected, $ post-> postedAt ()))? sant falskt; $ this-> assertTrue ($ matchningar); 

För att avsluta bestämmer vi om strängen returneras av postedAt () Metoden följer formatet "dag / månad / år". För en sådan kontroll används ett vanligt uttryck för att testa om mönstret \ D 2 \ / \ d 2 \ / \ d 4 ("2 nummer" + "bar" + "2 nummer" + "bar" + "4 siffror") är hittad.

Alternativt kan vi använda PHPUnits assertRegExp matcher.

Vid denna tidpunkt app / tester / modeller / PostTest.php filen är som följer:

// app / test / modeller / PostTest.php assertEquals ($ post-> author_id, $ post-> author-> id);  public function test_posted_at () // Instantiate, fyll med värden, spara och returnera $ post = FactoryMuff :: skapa ('Post'); // Regular expression som representerar d / m / Y mönster $ expected = '/ \ d 2 \ / \ d 2 \ / \ d 4 /'; // True om preg_match hittar mönstret $ matches = (preg_match ($ expected, $ post-> postedAt ()))? sant falskt; $ this-> assertTrue ($ matchningar); 

PS: Jag valde att inte skriva namnet på testen i CamelCase för läsbarhetsändamål. PSR-1 förlåter mig, men testRelationWithAuthor är inte lika läsbar som jag personligen föredrar. Du kan naturligtvis använda den stil du föredrar mest.

Sidmodell

Vårt CMS behöver en modell för att representera statiska sidor. Denna modell är implementerad enligt följande:

 'Required', // Sidtitel 'slug' => 'krävs' alpha_dash ', // Slug (url)' content '=>' krävs ', // Innehåll (markdown)' author_id '=>' krävs | numeric ' , // Författarens id); / ** * Array används av FactoryMuff * / public static $ factory = array ('title' => 'sträng', 'slug' => 'string', 'content' => 'text', 'author_id' => ' fabriks | Användare ', // Kommer vara ID för en existerande Användare.); / ** * Hör till användaren * / public function author () return $ this-> belongsTo ('User', 'author_id');  / ** * Ger menyn med cachen * * @returnsträngen Html för sidlänkar. * / offentliga statiska funktionen renderMenu () $ pages = Cache :: rememberForever ('pages_for_menu', funktion () return Page :: select (array ('titel', 'slug')) -> get () -> toArray ();); $ result = "; foreach ($ sidor som $ sida) $ result. = HTML :: action ('PagesController @ show', $ page ['title'], ['slug' => $ sida ['slug'] ]). ' | '; returnera $ result; / ** * Glöm cache när den sparas * / public function afterSave ($ success) om ($ success) Cache :: forget (' pages_for_menu '); / ** * Glöm cache när raderad * / public function delete () förälder :: radera (); Cache :: glömma ('pages_for_menu');

Vi kan observera att den statiska metoden, renderMenu (), gör ett antal länkar för alla befintliga sidor. Detta värde sparas i cachernyckeln, 'Pages_for_menu'. På så sätt, i framtiden, samtal till renderMenu (), det blir inte nödvändigt att slå den riktiga databasen. Detta kan ge avsevärda förbättringar av vår applikations prestanda.

Om en Sida sparas eller raderas (afterSave () och radera() metoder), kommer cachens värde att rensas, vilket orsakar renderMenu () för att återspegla den nya databasen. Så, om namnet på en sida ändras, eller om den är raderad, så nyckel "pages_for_menu" rensas från cacheminnet. (Cache :: glömma (pages_for_menu ');)

OBS: Metoden, afterSave (), är tillgängligt via Ardent-paketet. Annars skulle det vara nödvändigt att genomföra spara() Metod för att rengöra cacheminnet och samtalet förälder :: save ();

Sidtest

I: app / tester / modeller / PageTest.php, vi skriver följande tester:

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

Återigen har vi ett "valfritt" test för att bekräfta förhållandet. Som relationer är ansvaret för Illuminate \ Database \ Eloquent, som redan omfattas av Laravel egna test, behöver vi inte skriva ett annat test för att bekräfta att denna kod fungerar som förväntat.

 allmän funktion test_render_menu () $ pages = array (); för ($ 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));  // Kontrollera om cachen har skrivits $ this-> assertNotNull (Cache :: get ('pages_for_menu')); 

Detta är en av de viktigaste testerna för Sida modell. Först skapas fyra sidor i för slinga. Efter det, resultatet av renderMenu () samtal lagras i $ result variabel. Denna variabel ska innehålla en HTML-sträng som innehåller länkar till de befintliga sidorna.

De för varje loop kontrollerar om släkten (url) på varje sida finns i $ result. Detta räcker, eftersom HTML-exakt format inte är relevant för våra behov.

Slutligen bestämmer vi om cache-nyckeln, pages_for_menu, har lagrat något Med andra ord gjorde renderMenu () samtal sparas faktiskt värde till cacheminnet?

 allmän funktion test_clear_cache_after_save () // Ett testvärde sparas i cacheminnet :: put ('pages_for_menu', 'avalue', 5); // Det här ska städa värdet i cachen $ page = FactoryMuff :: skapa ('Sida'); $ This-> assertNull (Cache :: få (pages_for_menu ')); 

Detta test syftar till att verifiera om, när en ny lagras Sida, cachernyckeln 'Pages_for_menu' töms. De FactoryMuff :: skapa (sidan '); så småningom utlöser spara() metod, så det borde vara tillräckligt för nyckeln, 'Pages_for_menu', att rensas.

 allmän funktion test_clear_cache_after_delete () $ page = FactoryMuff :: skapa ('Sida'); // Ett testvärde sparas i cachemachin :: put ('pages_for_menu', 'value', 5); // Detta bör städa värdet i cachen $ page-> delete (); $ This-> assertNull (Cache :: få (pages_for_menu ')); 

På samma sätt som det föregående testet bestämmer denna om nyckeln 'Pages_for_menu' tömmes ordentligt efter att ha raderat en Sida.

Din PageTest.php ska se ut så här:

assertEquals ($ page-> author_id, $ page-> author-> id);  public function test_render_menu () $ pages = array (); för ($ 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));  // Kontrollera om cachen har skrivits $ this-> assertNotNull (Cache :: get ('pages_for_menu'));  allmän funktion test_clear_cache_after_save () // Ett testvärde sparas i cacheminnet :: put ('pages_for_menu', 'avalue', 5); // Det här ska städa värdet i cachen $ page = FactoryMuff :: skapa ('Sida'); $ This-> assertNull (Cache :: få (pages_for_menu '));  allmän funktion test_clear_cache_after_delete () $ page = FactoryMuff :: skapa ('sida'); // Ett testvärde sparas i cachemachin :: put ('pages_for_menu', 'value', 5); // Detta bör städa värdet i cachen $ page-> delete (); $ This-> assertNull (Cache :: få (pages_for_menu ')); 

Användarmodell

När det gäller de tidigare presenterade modellerna har vi nu Användare. Här är koden för den modellen:

 'string', 'email' => 'email', 'password' => '123123', 'password_confirmation' => '123123'); / ** * Har många sidor * / offentliga funktionssidor () return $ this-> hasMany ('Sida', 'author_id');  / ** * Har många inlägg * / offentliga funktionstjänster () return $ this-> hasMany ('Post', 'author_id'); 

Denna modell är frånvarande från tester.

Vi kan observera det, med undantag för relationer (vilket kan vara till hjälp för att testa) finns det ingen metodimplementering här. Vad sägs om autentisering? Tja, användningen av Confide-paketet ger redan genomförandet och testen för detta.

Testerna för Zizaco \ Confide \ ConfideUser finns i ConfideUserTest.php.

Det är viktigt att bestämma klassansvar innan du skriver dina test. Testa alternativet till "återställ lösenordet" av a Användare skulle vara överflödig. Detta beror på att det korrekta ansvaret för detta test ligger inom Zizaco \ Confide \ ConfideUser; inte i Användare.

Detsamma gäller för data valideringstest. Eftersom paketet, Ardent, hanterar det här ansvaret, skulle det inte vara så bra att testa funktionaliteten igen.

Kortfattat: Håll dina tester rena och organiserade. Bestäm det korrekta ansvaret för varje klass och testa bara vad som strikt är sitt ansvar.


Slutsats

Användningen av en minnesdatabas är en bra metod att snabbt utföra tester mot en databas. Tack vare hjälp från vissa paket, som Ardent, FactoryMuff och Confide, kan du minimera antalet koden i dina modeller samtidigt som testen är rena och objektiva.

I uppföljningen till den här artikeln granskar vi Kontrollant testning. Håll dig igång!

Låt oss börja med Laravel 4, låt oss lära dig det väsentliga!