Rammer gir et verktøy for rask applikasjonsutvikling, men ofte tilfaller teknisk gjeld så raskt som de tillater deg å skape funksjonalitet. Teknisk gjeld oppstår når vedlikeholdsevne ikke er et målrettet fokus for utvikleren. Fremtidige endringer og feilsøking blir kostbare på grunn av mangel på enhetstesting og struktur.
Slik begynner du å strukturere koden din for å oppnå testbarhet og vedlikehold - og spare tid.
La oss begynne med noen konstruerte, men typiske kode. Dette kan være en modellklasse i et gitt rammeverk.
klassen bruker offentlig funksjon getCurrentUser () $ user_id = $ _SESSION ['user_id']; $ user = App :: db-> velg ('id, brukernavn') -> hvor ('id', $ user_id) -> limit (1) -> get (); hvis ($ user-> num_results ()> 0) return $ user-> row (); returner falsk;
Denne koden vil fungere, men trenger forbedring:
$ _SESSION
global variabel. Unit-testing rammer, for eksempel PHPUnit, stole på kommandolinjen, hvor $ _SESSION
og mange andre globale variabler er ikke tilgjengelige.App :: db
brukes i vår søknad. Også, hva med tilfeller hvor vi ikke vil bare den nåværende brukerens informasjon?Her er et forsøk på å opprette en enhetstest for funksjonen ovenfor.
klasse UserModelTest utvider PHPUnit_Framework_TestCase public function testGetUser () $ user = new User (); $ currentUser = $ user-> getCurrentUser (); $ this-> assertEquals (1, $ currentUser-> id);
La oss undersøke dette. Først vil testen mislykkes. De $ _SESSION
variabel som brukes i Bruker
objekt eksisterer ikke i en enhetstest, da det kjører PHP i kommandolinjen.
For det andre er det ingen oppsett for databasetilkobling. Dette betyr at vi for å gjøre dette arbeidet må starte opp vår søknad for å få det app
objekt og dets db
gjenstand. Vi trenger også en arbeidsdatabaseforbindelse for å teste mot.
For å få denne enheten til å teste, må vi:
Så la oss finne ut hvordan vi kan forbedre dette.
Funksjonen som henter den nåværende brukeren er unødvendig i denne enkle sammenhengen. Dette er et opptatt eksempel, men i DRY-prinsippens forstand, er den første optimaliseringen jeg velger å lage, å generalisere denne metoden.
klassen bruker offentlig funksjon getUser ($ user_id) $ user = App :: db-> velg ('bruker') -> hvor ('id', $ user_id) -> limit (1) -> get (); hvis ($ user-> num_results ()> 0) return $ user-> row (); returner falsk;
Dette gir en metode vi kan bruke over hele vårt program. Vi kan passere i den nåværende brukeren på tidspunktet for samtalen, i stedet for å overføre den funksjonaliteten til modellen. Koden er mer modulær og vedlikeholdsbar når den ikke stole på andre funksjoner (som økt global variabel).
Dette er imidlertid fortsatt ikke testbar og vedlikeholdsbar som det kan være. Vi stoler fortsatt på databasetilkoblingen.
La oss bidra til å forbedre situasjonen ved å legge til litt avhengighetsinjeksjon. Her er hva vår modell kan se ut når vi overfører databasekunden til klassen.
klassen bruker protected $ _db; offentlig funksjon __construct ($ db_connection) $ dette -> _ db = $ db_connection; offentlig funksjon getUser ($ user_id) $ user = $ this -> _ db-> velg ('bruker') -> hvor ('id', $ user_id) -> limit (1) -> get (); hvis ($ user-> num_results ()> 0) return $ user-> row (); returner falsk;
Nå, avhengighetene til vår Bruker
modell er gitt for. Vår klasse tar ikke lenger ut en viss databaseforbindelse, og heller ikke på noen globale objekter.
På dette punktet er vår klasse i utgangspunktet testbar. Vi kan passere i en datakilde av vårt valg (for det meste) og et bruker-ID, og teste resultatene av samtalen. Vi kan også slå ut separate databaseforbindelser (forutsatt at begge implementerer de samme metodene for å hente data). Kul.
La oss se på hva en enhetstest kan se ut for det.
_mockDb (); $ user = ny bruker ($ db_connection); $ result = $ user-> getUser (1); $ forventet = ny StdClass (); $ forventet-> id = 1; $ forventet-> brukernavn = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'Bruker-ID satt riktig'); $ this-> assertEquals ($ result-> brukernavn, $ forventet-> brukernavn, 'Brukernavn satt riktig'); beskyttet funksjon _mockDb () // "Mock" (stub) database rad resultatobjekt $ returnResult = new StdClass (); $ returnResult-> id = 1; $ returnResult-> brukernavn = 'fideloper'; // Mock database result object $ result = m :: mock ('DbResult'); $ result-> shouldReceive ('num_results') -> once () -> ogReturn (1); $ result-> shouldReceive ('row') -> once () -> ogReturn ($ returnResult); // Mock databaseforbindelsesobjekt $ db = m :: mock ('DbConnection'); $ db-> shouldReceive ('select') -> en gang () -> ogReturn ($ db); $ db-> shouldReceive ('where') -> once () -> andReturn ($ db); $ db-> shouldReceive ('limit') -> en gang () -> ogReturn ($ db); $ db-> shouldReceive ('get') -> en gang () -> ogReturn ($ resultat); returner $ db;
Jeg har lagt til noe nytt for denne testen: Mockery. Mockery lar deg "mock" (falske) PHP-objekter. I dette tilfellet mocker vi databasetilkoblingen. Med vår mock kan vi hoppe over å teste en databaseforbindelse og bare teste modellen vår.
Ønsker å lære mer om Mockery?
I dette tilfellet mocker vi en SQL-tilkobling. Vi forteller det mocke objektet å forvente å ha å velge
, hvor
, grense
og få
metoder kalt på den. Jeg returnerer Mock, i seg selv, for å speile hvordan SQL-forbindelsesobjektet returnerer seg selv ($ dette
), slik at metoden kalles "chainable". Merk at, for få
metode, returnerer jeg databasen samtale resultatet - a stdClass
objekt med brukerdata som er fylt ut.
Dette løser noen problemer:
Vi kan fortsatt gjøre mye bedre. Her blir det interessant.
For å forbedre dette videre kunne vi definere og implementere et grensesnitt. Vurder følgende kode.
grensesnitt UserRepositoryInterface offentlig funksjon getUser ($ user_id); klasse MysqlUserRepository implementerer UserRepositoryInterface protected $ _db; offentlig funksjon __construct ($ db_conn) $ dette -> _ db = $ db_conn; offentlig funksjon getUser ($ user_id) $ user = $ this -> _ db-> velg ('bruker') -> hvor ('id', $ user_id) -> limit (1) -> get (); hvis ($ user-> num_results ()> 0) return $ user-> row (); returner falsk; klasse Bruker protected $ userStore; offentlig funksjon __construct (UserRepositoryInterface $ bruker) $ this-> userStore = $ user; offentlig funksjon getUser ($ user_id) return $ this-> userStore-> getUser ($ user_id);
Det skjer noen ting her.
adduser ()
metode.Brukergrensesnitt
i vår Bruker
modell. Dette garanterer at datakilden alltid vil ha en getUser ()
metode tilgjengelig, uansett hvilken datakilde som brukes til å implementere Brukergrensesnitt
.Legg merke til at vår
Bruker
Objekttype-tipsBrukergrensesnitt
i sin konstruktør. Dette betyr at en klasse implementeringBrukergrensesnitt
Må sendes inn iBruker
gjenstand. Dette er en garanti vi stoler på - vi trengergetUser
metode for alltid å være tilgjengelig.
Hva er resultatet av dette?
Bruker
klasse, kan vi enkelt mocke datakilden. (Testing av implementeringen av datakilden ville være jobben med en separat enhetstest).Bruker
objekt hvis vi trenger det. Hvis du bestemmer deg for å dike SQL, kan du bare opprette en annen implementering (for eksempel, MongoDbUser
) og send det inn i din Bruker
modell.Vi har også forenklet vår enhetstest!
_mockUserRepo (); $ user = ny bruker ($ userRepo); $ result = $ user-> getUser (1); $ forventet = ny StdClass (); $ forventet-> id = 1; $ forventet-> brukernavn = 'fideloper'; $ this-> assertEquals ($ result-> id, $ expected-> id, 'Bruker-ID satt riktig'); $ this-> assertEquals ($ result-> brukernavn, $ forventet-> brukernavn, 'Brukernavn satt riktig'); beskyttet funksjon _mockUserRepo () // Mock forventet resultat $ result = new StdClass (); $ resultat-> id = 1; $ resultat-> brukernavn = 'fideloper'; // Mock ethvert brukerregister $ userRepo = m :: mock ('Fideloper \ Third \ Repository \ UserRepositoryInterface'); $ userRepo-> shouldReceive ('getUser') -> en gang () -> ogReturn ($ resultat); returner $ userRepo;
Vi har tatt arbeidet med å mocking en databaseforbindelse ut helt. I stedet mocker vi bare datakilden, og forteller det hva du skal gjøre når getUser
er kalt.
Men vi kan fortsatt gjøre det bedre!
Vurder bruk av vår nåværende kode:
// I noen kontroller $ user = ny bruker (ny MysqlUser (App: db-> getConnection ("mysql"))); $ user-> id = App :: økt ("bruker-> id"); $ currentUser = $ user-> getUser ($ user_id);
Vårt siste skritt vil være å introdusere beholdere. I koden ovenfor må vi opprette og bruke en gjeng med objekter bare for å få vår nåværende bruker. Denne koden kan være strøget i hele applikasjonen din. Hvis du trenger å bytte fra MySQL til MongoDB, vil du fortsatt må redigeres hvert sted der koden ovenfor vises. Det er neppe tørt. Beholdere kan fikse dette.
En beholder inneholder "en" et objekt eller en funksjonalitet. Det ligner på et register i søknaden din. Vi kan bruke en beholder til å automatisere automatisk en ny Bruker
objekt med alle nødvendige avhengigheter. Nedenfor bruker jeg Pimple, en populær containerklasse.
// Et sted i en konfigurasjonsfil $ container = new Pimple (); $ container ["user"] = function () return ny bruker (ny MysqlUser (App: db-> getConnection ('mysql'))); // Nå, i alle våre kontrollører kan vi bare skrive: $ currentUser = $ container ['user'] -> getUser (App :: session ('user_id'));
Jeg har flyttet etableringen av Bruker
modell til ett sted i applikasjonskonfigurasjonen. Som et resultat:
Bruker
objekt og datalageret er valgt på ett sted i vår søknad.Bruker
modell fra å bruke MySQL til en annen datakilde i EN plassering. Dette er mye mer vedlikeholdsdyktig.I løpet av denne opplæringen oppnådde vi følgende:
Jeg er sikker på at du har lagt merke til at vi har lagt til mye mer kode i navnet vedlikehold og testbarhet. Et sterkt argument kan gjøres mot denne gjennomføringen: vi øker kompleksiteten. Faktisk krever dette en dypere kunnskap om kode, både for hovedforfatteren og for samarbeidspartnere i et prosjekt.
Imidlertid er kostnaden for forklaring og forståelse langt utvekt av det ekstra generelle avta i teknisk gjeld.
Du kan inkludere hån og PHPUnit inn i søknaden din enkelt ved å bruke Komponist. Legg til disse i "Require-dev" -delen din i din composer.json
fil:
"krav-dev": "hån / hån": "0.8. *", "phpunit / phpunit": "3.7. *"
Du kan deretter installere Composer-baserte avhengigheter med dev-kravene:
$ php composer.phar installere --dev
Lær mer om Mockery, Composer og PHPUnit her på Nettuts+.
For PHP, bruk Laravel 4, da det gjør eksepsjonell bruk av containere og andre konsepter skrevet om her.
Takk for at du leste!