Ramar skapar ett verktyg för snabb applikationsutveckling, men genererar ofta tekniska skulder så snabbt som de låter dig skapa funktionalitet. Teknisk skuld skapas när underhållsbehov inte är ett målmedvetet fokus hos utvecklaren. Framtida förändringar och felsökning blir kostsamma på grund av brist på enhetstestning och struktur.
Så här börjar du strukturera din kod för att uppnå testbarhet och underhåll - och spara tid.
Låt oss börja med några konstruerade, men typiska kod. Detta kan vara en modellklass i en given ram.
klass användare offentlig funktion getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> välj ('id, användarnamn') -> var ('id', $ user_id) -> limit (1) -> get (); om ($ user-> num_results ()> 0) return $ user-> row (); returnera false;
Denna kod kommer att fungera, men behöver förbättras:
$ _SESSION
global variabel. Enhetsprovningsramar, till exempel PHPUnit, är beroende av kommandoraden, där $ _SESSION
och många andra globala variabler är inte tillgängliga.App :: db
används i vår ansökan. Också vad gäller instanser där vi inte bara vill ha den nuvarande användarens information?Här är ett försök att skapa ett enhetstest för ovanstående funktionalitet.
klass UserModelTest utökar PHPUnit_Framework_TestCase public function testGetUser () $ user = new User (); $ currentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ currentUser-> id);
Låt oss undersöka detta. Först kommer testet att misslyckas. De $ _SESSION
variabel som används i Användare
objekt existerar inte i ett enhetstest, eftersom det kör PHP i kommandoraden.
För det andra finns det ingen databasanslutning setup. Det betyder att vi måste starta vår ansökan för att kunna få det för att kunna göra detta arbete App
objekt och dess db
objekt. Vi behöver också en arbetsdatabasanslutning för att testa mot.
För att göra det här testet arbete, skulle vi behöva:
Så, låt oss ta reda på hur vi kan förbättra detta.
Funktionen som hämtar den nuvarande användaren är onödig i detta enkla sammanhang. Detta är ett konstruerat exempel, men i DRY-principens anda, den första optimeringen jag väljer att göra är att generalisera denna metod.
klass användare allmän funktion getUser ($ user_id) $ user = App :: db-> välj ('användare') -> where ('id', $ user_id) -> limit (1) -> get (); om ($ user-> num_results ()> 0) return $ user-> row (); returnera false;
Detta ger en metod som vi kan använda över hela vår ansökan. Vi kan vidarebefordra den aktuella användaren vid samtalets gång, istället för att överföra den funktionen till modellen. Koden är mer modulär och underhållbar när den inte är beroende av andra funktioner (till exempel den globala variabelen).
Detta är dock fortfarande inte testbart och underhållbart som det kan vara. Vi är fortfarande beroende av databasanslutningen.
Låt oss hjälpa till att förbättra situationen genom att lägga till viss beredskapsinjektion. Här är vad vår modell kan se ut, när vi överför databasförbindelsen till klassen.
klass användare protected $ _db; offentlig funktion __construct ($ db_connection) $ this -> _ db = $ db_connection; allmän funktion getUser ($ user_id) $ user = $ this -> _ db-> select ('användare') -> var ('id', $ user_id) -> limit (1) -> get (); om ($ user-> num_results ()> 0) return $ user-> row (); returnera false;
Nu beror våra Användare
modell finns för. Vår klass tar inte längre ut en viss databasförbindelse, och bygger inte heller på några globala objekt.
Vid denna tidpunkt är vår klass i princip testbar. Vi kan skicka in en datakälla efter eget val (mestadels) och ett användar-ID, och testa resultaten av samtalet. Vi kan också byta ut separata databasanslutningar (förutsatt att båda implementerar samma metoder för att hämta data). Häftigt.
Låt oss titta på vad ett enhetstest kan se ut för det.
_mockDb (); $ user = ny användare ($ db_connection); $ result = $ user-> getUser (1); $ expected = new StdClass (); $ expected-> id = 1; $ expected-> användarnamn = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'User ID set correctly'); $ this-> assertEquals ($ result-> användarnamn, $ förväntat-> användarnamn, 'Användarnamn satt korrekt'); skyddad funktion _mockDb () // "Mock" (stub) databasrad resultatobjekt $ returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> användarnamn = 'fideloper'; // Mock databasresultatobjekt $ result = m :: mock ('DbResult'); $ result-> shouldReceive ('num_results') -> once () -> ochReturn (1); $ result-> shouldReceive ('row') -> en gång () -> ochReturn ($ returnResult); // Mock-databasanslutningsobjekt $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('select') -> en gång () -> ochReturn ($ db); $ db-> shouldReceive ('where') -> once () -> andReturn ($ db); $ db-> shouldReceive ('limit') -> en gång () -> ochReturn ($ db); $ db-> shouldReceive ('get') -> en gång () -> ochReturn ($ resultat); returnera $ db;
Jag har lagt till något nytt för detta test: Mockery. Mockery låter dig "spotta" (falska) PHP-objekt. I det här fallet mockar vi databasanslutningen. Med vår mock kan vi hoppa över testning av en databasanslutning och helt enkelt testa vår modell.
Vill du lära dig mer om Mockery?
I det här fallet mockar vi en SQL-anslutning. Vi berättar att det mocka objektet förväntar sig att ha Välj
, var
, begränsa
och skaffa sig
metoder som kallas på det. Jag återvänder Mock, självt, för att spegla hur SQL-kopplingsobjektet återvänder sig ($ detta
), vilket gör att dess metod kallas "chainable". Observera att för skaffa sig
metod returnerar jag databasuppkopplingsresultatet - a stdClass
objekt med användardata som är fyllda.
Detta löser några problem:
Vi kan fortfarande göra mycket bättre. Här är det som blir intressant.
För att förbättra detta ytterligare kunde vi definiera och implementera ett gränssnitt. Tänk på följande kod.
gränssnitt UserRepositoryInterface public function getUser ($ user_id); klass MysqlUserRepository implementerar UserRepositoryInterface protected $ _db; offentlig funktion __construct ($ db_conn) $ this -> _ db = $ db_conn; allmän funktion getUser ($ user_id) $ user = $ this -> _ db-> select ('användare') -> var ('id', $ user_id) -> limit (1) -> get (); om ($ user-> num_results ()> 0) return $ user-> row (); returnera false; klass User protected $ userStore; offentlig funktion __construct (UserRepositoryInterface $ user) $ this-> userStore = $ user; allmän funktion getUser ($ user_id) return $ this-> userStore-> getUser ($ user_id);
Det händer några saker här.
Lägg till användare()
metod.Användargränssnitt
i vår Användare
modell. Detta garanterar att datakällan alltid kommer att ha en getUser ()
tillgänglig metod, oavsett vilken datakälla som används för att genomföra Användargränssnitt
.Observera att vår
Användare
Objekttyp-tipsAnvändargränssnitt
i sin konstruktör. Detta innebär att en klass genomförandeAnvändargränssnitt
MÅSTE gå in iAnvändare
objekt. Det här är en garanti som vi lita på - vi behövergetUser
metod att alltid vara tillgänglig.
Vad är resultatet av detta?
Användare
klass kan vi enkelt mocka datakällan. (Testa implementeringen av datakällan skulle vara jobbet med ett separat enhetstest).Användare
objekt om vi behöver. Om du bestämmer dig för att dölja SQL, kan du bara skapa en annan implementering (till exempel, MongoDbUser
) och skicka det till din Användare
modell.Vi har också förenklat vårt test på enheten!
_mockUserRepo (); $ user = new User ($ userRepo); $ result = $ user-> getUser (1); $ expected = new StdClass (); $ expected-> id = 1; $ expected-> användarnamn = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'User ID set correctly'); $ this-> assertEquals ($ result-> användarnamn, $ förväntat-> användarnamn, 'Användarnamn satt korrekt'); skyddad funktion _mockUserRepo () // Mock förväntat resultat $ result = new StdClass (); $ resultat-> id = 1; $ result-> användarnamn = 'fideloper'; // Mock något användarregister $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> en gång () -> ochReturn ($ resultat); returnera $ userRepo;
Vi har arbetat med att mocka en databasanslutning helt ut. I stället stöter vi bara på datakällan och berättar vad som ska göras när getUser
kallas.
Men vi kan fortfarande göra det bättre!
Tänk på användningen av vår nuvarande kod:
// I någon controller $ user = new User (ny MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: session ("user-> id"); $ currentUser = $ user-> getUser ($ user_id);
Vårt sista steg kommer att vara att introducera behållare. I ovanstående kod måste vi skapa och använda en massa objekt bara för att få vår nuvarande användare. Den här koden kan vara skräpad över din ansökan. Om du behöver byta från MySQL till MongoDB, så kommer du fortfarande måste redigera varje ställe där ovanstående kod visas. Det är knappast torkat. Behållare kan fixa detta.
En behållare innehåller "ett" objekt eller funktionalitet. Det liknar ett register i din ansökan. Vi kan använda en behållare för att automatiskt skapa en ny Användare
objekt med alla nödvändiga beroenden. Nedan använder jag Pimple, en populär containerklass.
// Någonstans i en konfigurationsfil $ container = new Pimple (); $ container ["user"] = function () returnera ny användare (ny MysqlUser (App: db-> getConnection ('mysql'))); // Nu kan vi i alla våra kontroller skriva: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
Jag har flyttat skapandet av Användare
modell till en plats i applikationskonfigurationen. Som ett resultat:
Användare
objekt och valfri datalagring är definierad på en plats i vår ansökan.Användare
modell från att använda MySQL till någon annan datakälla i ETT plats. Detta är mycket mer underhållbart.Under den här handledningen genomförde vi följande:
Jag är säker på att du har märkt att vi har lagt till mycket mer kod i namn av underhållbarhet och testbarhet. Ett starkt argument kan göras mot denna implementering: vi ökar komplexiteten. Detta kräver faktiskt en djupare kunskap om kod, både för huvudförfattaren och för samarbetspartners av ett projekt.
Kostnaden för förklaring och förståelse är dock väldigt utvägd av det extra övergripande minska i teknisk skuld.
Du kan inkludera Hån och PHPUnit till din ansökan enkelt med hjälp av kompositören. Lägg till dessa i din "Require-dev" -sektion i din composer.json
fil:
"kräver-dev": "hån": "0.8. *", "phpunit / phpunit": "3.7. *"
Du kan sedan installera dina Composer-baserade beroenden med "dev" -kraven:
$ php composer.phar installera --dev
Läs mer om Mockery, Composer och PHPUnit här på Nettuts+.
För PHP, överväga att använda Laravel 4, eftersom det gör exceptionell användning av behållare och andra begrepp skrivna om här.
Tack för att du läser!