Det er en uheldig sannhet at, selv om grunnprinsippet bak testingen er ganske enkelt, er det vanskeligere å introdusere denne prosessen i din daglige kodings arbeidsflyt enn du kanskje håper. De forskjellige jargongene kan være overveldende! Heldigvis har en rekke verktøy ryggen din, og bidra til å gjøre prosessen så enkelt som mulig. Mockery, den fremste mock objekt ramme for PHP, er et slikt verktøy!
I denne artikkelen vil vi grave inn i hva mocking er, hvorfor det er nyttig, og hvordan å integrere Mockery i testprosessen din.
En mock gjenstand er ikke noe mer enn et lite testjargong som refererer til å simulere virkemåten til virkelige objekter. I enklere termer, ofte, når du tester, vil du ikke ønske å utføre en bestemt metode. I stedet må du bare sørge for at det faktisk ble kalt.
Kanskje et eksempel er i orden. Tenk deg at koden utløser en metode som vil logge litt data til en fil. Når du tester denne logikken, vil du absolutt ikke fysisk berøre filsystemet. Dette har potensial til å drastisk redusere hastigheten på testene dine. I disse situasjonene er det best å mocke filsystemet ditt, og i stedet for å lese filen manuelt for å bevise at den ble oppdatert, bare forsikre deg om at den aktuelle metoden på klassen faktisk ble kalt. Dette er mocking! Det er ikke noe mer enn det; simulere oppførselen til objekter.
Husk: sjargong er bare sjargong. Ikke la et forvirrende stykke terminologi være for å avskrekke deg fra å lære en ny ferdighet.
Spesielt når utviklingsprosessen din modnes - inkludert å omfatte det enkeltansvarlige prinsippet og utnytte avhengighetsinjeksjon - vil en fortrolighet med mocking raskt bli avgjørende.
Mocks vs Stubs: Sjansene er høye at du ofte vil høre vilkårene, håne og stub, kastet om utveksling. Faktisk tjener de to forskjellige formål. Den tidligere refererer til prosessen med å definere forventninger og sikre ønsket oppførsel. Med andre ord kan en mock potensielt føre til en mislykket test. En stub, derimot, er rett og slett et dummy sett med data som kan overføres for å oppfylle visse kriterier.
Defacto-testbiblioteket for PHP, PHPUnit, sender med sin egen API for mocking objekter; men dessverre kan det være tungvint å jobbe med. Som du sikkert er klar over, jo vanskeligere testingen er, jo mer sannsynlig er det at utvikleren bare (og dessverre) ikke vil.
Heldigvis finnes en rekke tredjepartsløsninger gjennom Packagist (Composer's pakkearkiv), noe som gjør det mulig å øke lesbarheten, og enda viktigere, å skrive på. Blant disse løsningene - og mest bemerkelsesverdige av settet - er Mockery, et rammeverk-agnostisk mock objekt rammeverk.
Designet som et drop-in-alternativ for de som er overveldet av PHPUnit's mocking verbosity, er Mockery et enkelt, men kraftig verktøy. Som du sikkert vil finne, er det faktisk industristandarden for moderne PHP-utvikling.
Som de fleste PHP-verktøy i disse dager, er den anbefalte metoden for å installere Mockery gjennom Composer (selv om den er tilgjengelig gjennom Pear også).
Vent, hva er dette Composer-tingen? Det er PHP-fellesskapets foretrukne verktøy for avhengighetsadministrasjon. Det gir en enkel måte å erklære et prosjekts avhengigheter til, og trekke dem inn med en enkelt kommando. Som en moderne PHP-utvikler er det viktig at du har en grunnleggende forståelse av hva Composer er, og hvordan du bruker den.
Hvis du arbeider sammen, legger du til et nytt for læringsformål composer.json
filen til et tomt prosjekt og legg til:
"require-dev": "hån / hån": "dev-master"
Denne biten av JSON angir at for søknad krever programmet Mockery-biblioteket. Fra kommandolinjen, a komponent installere --dev
vil trekke inn pakken.
$ komponent installere --dev Laster kompositorbeholdninger med pakkeinformasjon Installere avhengigheter (inkludert krav-dev) - Installere hån / mockery (dev-master 5a71299) Kloning 5a712994e1e3ee604b0d355d1af342172c6f475f Skrive låsfil Generere autoload-filer
Som en ekstra bonus, sender Composer med egen autoloader gratis! Enten angi en klassekart med kataloger og
komponist dump-autoload
, eller følg PSR-0-standarden og juster katalogstrukturen for å matche. Se Nettuts + for å lære mer. Hvis du fremdeles manuelt krever utallige filer i hver PHP-fil, kan du bare gjøre det galt.
Før vi kan implementere en løsning, er det best å først vurdere problemet. Tenk deg at du må implementere et system for å håndtere prosessen med å generere innhold og skrive det til en fil. Kanskje genererer generatoren ulike data, enten fra lokale filstubber eller en webtjeneste, og deretter blir dataene skrevet til filsystemet.
Hvis du følger prinsippet om enkeltansvar - som dikterer at hver klasse burde være ansvarlig for nøyaktig én ting - da står det grunnen til at vi skal dele denne logikken i to klasser: en for å generere det nødvendige innholdet, og en annen for fysisk å skrive dataene til en fil. EN Generator
og Fil
klasse, henholdsvis, bør gjøre trikset.
Tips: Hvorfor ikke bruke
file_put_contents
direkte fraGenerator
klasse? Vel, spør deg selv: "Hvordan kan jeg teste dette??"Det finnes teknikker som ape-patching, som kan tillate deg å overbelaste slike ting, men som en god praksis er det bedre å isteden vikle slik funksjonalitet opp, slik at det lett kan spotles med verktøy som Mockery!
Her er en grunnleggende struktur (med en sunn dose pseudokode) for vår Generator
klasse.
fil = $ fil; beskyttet funksjon getContent () // forenklet for demo retur 'foo bar'; offentlig funksjon brann () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ innhold);
Denne koden utnytter det vi refererer til som avhengighetsinjeksjon. Igjen, dette er bare utviklerjargong for å injisere en klasses avhengigheter gjennom sin konstruktormetode, i stedet for hardkoding av dem.
Hvorfor er dette gunstig? Fordi ellers ikke ville vi kunne mocke Fil
klasse! Jo, vi kunne mocke på Fil
klassen, men hvis det er hardkodd i den klassen vi tester, er det ingen enkel måte å erstatte den forekomsten med den spottede versjonen.
offentlig funksjon __construct () // anti-mønster $ this-> file = new File;
Den beste måten å bygge testbar søknad på, er å nærme seg hver ny metodeanrop med spørsmålet "Hvordan kan jeg teste dette??"Mens det er triks for å komme seg rundt denne hardkodingen, er det allment vurdert å være en dårlig praksis. I stedet skal du alltid injisere en klasses avhengighet gjennom konstruktøren eller via setter injeksjon.
Setter injeksjon er mer eller mindre identisk med konstruktørinjeksjon. Prinsippet er akkurat det samme; Den eneste forskjellen er at det heller er å injisere klassens avhengigheter gjennom sin konstruktormetode, men i stedet gjøres det gjennom en setter-metode, slik som:
offentlig funksjon setFile (File $ file) $ this-> file = $ file;
En felles kritikk av avhengighetsinjeksjon er at den introduserer ytterligere kompleksitet i en applikasjon, alt for å gjøre det mer testbart. Selv om kompleksitetsargumentet er diskutabelt i denne forfatterens oppfatning, kan du, hvis du foretrekker det, tillate avhengighetsinjeksjon, mens du fremdeles angir tilbakebetalingsstandarder. Her er et eksempel:
klasse generator offentlig funksjon __construct (fil $ file = null) $ this-> file = $ file?: ny fil;
Nå, hvis en forekomst av Fil
blir sendt videre til konstruktøren, vil objektet bli brukt i klassen. På den annen side, hvis ingenting er bestått, Generator
vil falle tilbake å manuelt instantiere den aktuelle klassen. Dette tillater slike variasjoner som:
# Klasse instantiates Fil ny generator; # Injiser fil ny generator (ny fil); # Injiser en mock av File for å teste ny generator ($ mockedFile);
Fortsetter på, i forbindelse med denne opplæringen, Fil
klassen vil ikke være noe mer enn et enkelt omslag rundt PHP file_put_contents
funksjon.
Snarere enkelt, eh? La oss skrive en test for å se, førstehånds, hva problemet er.
Brann();Vær oppmerksom på at disse eksemplene antar at de nødvendige klassene blir autoloaded med Composer. Din
composer.json
filen aksepterer eventuelt enAutostart
objekt, der du kan spesifisere hvilke kataloger eller klasser du vil autoload. Ikke mer rotetekrever
uttalelser!Hvis du arbeider sammen, kjører
PHPUnit
vil returnere:OK (1 test, 0 påstander)Det er grønt; det betyr at vi kan gå videre til neste oppgave, ikke sant? Vel, ikke akkurat. Selv om det er sant at koden egentlig fungerer, hver gang denne testen kjøres, a
Selv om testene passerer, berører de feil filsystemet.foo.txt
filen vil bli opprettet på filsystemet. Hva med når du har skrevet dusinvis flere tester? Som du kan tenke deg, vil testens hastighet på utførelsen raskt stramme.Fortsatt ikke overbevist? Hvis redusert testhastighet ikke svinger deg, så betrakt sunn fornuft. Tenk på det: vi tester
Generator
klasse; hvorfor har vi noen interesse i å utføre kode fraFil
klasse? Det bør ha sine egne tester! Hvorfor pokker ville vi doble opp?
Løsningen
Forhåpentligvis ga den forrige delen den perfekte illustrasjonen for hvorfor mocking er viktig. Som det ble nevnt tidligere, selv om vi kunne bruke PHPUnits native API for å betjene våre krav til mocking, er det ikke altfor hyggelig å jobbe med. For å illustrere denne sannheten, er et eksempel for å hevde at et mocked objekt skulle få en metode,
getName
og returnereJohn Doe
.offentlige funksjon testNativeMocks () $ mock = $ this-> getMock ('SomeClass'); $ mock-> expects ($ this-> once ()) -> metode ('getName') -> vil ($ this-> returnValue ('John Doe'));Mens det blir jobben gjort - hevder at a
getName
Metoden kalles en gang, og returnerer John Doe - PHPUnits implementering er forvirrende og verbose. Med Mockery kan vi drastisk forbedre lesbarheten.offentlig funksjon testMockery () $ mock = Mockery :: mock ('SomeClass'); $ mock-> shouldReceive ('getName') -> once () -> andReturn ('John Doe');Legg merke til hvordan sistnevnte eksempel leser (og snakker) bedre.
Fortsetter med eksemplet fra forrige "Dilemma delen, denne gangen, innenfor
GeneratorTest
klasse, la oss isteden spotse - eller simulere oppførselen til -Fil
klasse med Mockery. Her er den oppdaterte koden:shouldReceive ('put') -> med ('foo.txt', 'foo bar') -> en gang (); $ generator = ny generator ($ mockedFile); $ Generator-> brann ();Forvirret av
Hån :: close ()
referanse innenforrive ned
metode? Dette statiske anropet rydder opp Mockery-beholderen som brukes av den nåværende testen, og kjører eventuelle verifikasjonsoppgaver som trengs for dine forventninger.En klasse kan bli stappet ved hjelp av den lesbare
Hån :: mock ()
metode. Deretter må du typisk angi hvilke metoder på denne spot-objektet du forventer å bli kalt, sammen med eventuelle relevante argumenter. Dette kan oppnås, viashouldReceive (METODE)
ogmed (ARG)
fremgangsmåter.I dette tilfellet, når vi ringer
$ Generate-> brann ()
, Vi hevder at det skal ringe tilsette
metode påFil
eksempel, og send den banen,foo.txt
, og dataene,foo bar
.// biblioteker / Generator.php offentlig funksjon brann () $ content = $ this-> getContent (); $ this-> file-> put ('foo.txt', $ innhold);Fordi vi bruker avhengighetsinjeksjon, er det nå en kino å isteden injisere den spottede
Fil
gjenstand.$ generator = ny generator ($ mockedFile);Hvis vi kjører testene igjen, vil de fortsatt komme grønne, men
Fil
klasse - og dermed filsystemet - vil aldri bli rørt! Igjen er det ikke nødvendig å røreFil
. Det bør ha sine egne tester! Mocking for seieren!Simple Mock Objects
Mock objekter trenger ikke alltid å referere til en klasse. Hvis du bare trenger en enkel gjenstand, kanskje for en bruker, kan du passere en matrise til
håne
metode - hvor for hvert element, nøkkelen og verdien tilsvarer metodens navn og returverdi, henholdsvis.offentlig funksjon testSimpleMocks () $ user = Mockery :: mock (['getFullName' => 'Jeffrey Way']); $ Bruker-> getFullName (); // Jeffrey WayReturner verdier fra stappede metoder
Det vil sikkert være tider når en mocked klassemetode må returnere en verdi. Fortsett med vår Generator / File eksempel, hva om vi må sikre at hvis filen allerede eksisterer, bør den ikke overskrives? Hvordan kan vi oppnå det?
Nøkkelen er å bruke
andReturn ()
Metode på din mocked objekt å simulere forskjellig stater. Her er et oppdatert eksempel:offentlig funksjon testDoesNotOverwriteFile () $ mockedFile = Mockery :: mock ('File'); $ mockedFile-> shouldReceive ('exists') -> once () -> andReturn (true); $ mockedFile-> shouldReceive ('put') -> never (); $ generator = ny generator ($ mockedFile); $ Generator-> brann ();Denne oppdaterte koden anfører nå at en
finnes
Metoden skal utløses på spottenFil
klassen, og det skal, med henblikk på denne testens sti, komme tilbakeekte
, signalerer at filen allerede eksisterer og ikke skal overskrives. Vi sørger nå for at i slike situasjoner,sette
metode påFil
klassen utløses aldri. Med Mockery er dette enkelt, takket værealdri()
forventning.$ mockedFile-> shouldReceive ('put') -> never ();Skulle vi kjøre testene igjen, vil en feil bli returnert:
Metode eksisterer () fra Fil skal kalles nøyaktig 1 ganger, men kalt 0 ganger.Aha; så testen forventet det
$ Dette-> Fil-> finnes ()
skal kalles, men det skjedde aldri. Som sådan mislyktes det. La oss fikse det!fil = $ fil; beskyttet funksjon getContent () // forenklet for demo retur 'foo bar'; offentlig funksjon brann () $ content = $ this-> getContent (); $ file = 'foo.txt'; hvis (! $ this-> file-> exists ($ file)) $ this-> file-> put ($ file, $ content);Det er alt der er til det! Ikke bare har vi fulgt en TDD (testdrevet utvikling) syklus, men testene er tilbake til grønt!
Det er viktig å huske at denne teststesten bare er effektiv hvis du faktisk gjør testen avhengig av klassen din også! Ellers, selv om testene kan vise grønt, for produksjon, vil koden bryte. Vår demonstrasjon så langt har bare sikret det
Generator
fungerer som forventet. Ikke glem å testeFil
også!
forventninger
La oss grave litt mer dypt inn i Mockery's forventningserklæringer. Du er allerede kjent med
shouldReceive
. Vær forsiktig med dette, skjønt; navnet heter litt misvisende. Når det er igjen på egenhånd, krever det ikke at metoden skal utløses. standard er null eller flere ganger (zeroOrMoreTimes ()
). For å hevde at du krever at metoden blir kalt en gang, eller muligens flere ganger, finnes det en rekke alternativer:$ Blindprøve-> shouldReceive ( 'metode') -> en gang (); $ Blindprøve-> shouldReceive ( 'metoden') -> ganger (1); $ Blindprøve-> shouldReceive ( 'metode') -> ihvertfall () -> ganger (1);Det vil være tider når ytterligere begrensninger er nødvendige. Som vist tidligere, kan dette være spesielt nyttig når du må sørge for at en bestemt metode utløses med de nødvendige argumentene. Det er viktig å huske på at forventningen bare gjelder hvis en metode kalles med disse nøyaktige argumentene.
Her er noen eksempler.
$ Blindprøve-> shouldReceive ( 'get') -> withAnyArgs () -> én gang (); // standard $ mock-> shouldReceive ('get') -> med ('foo.txt') -> once (); $ mock-> shouldReceive ('put') -> med ('foo.txt', 'foo bar') -> en gang ();Dette kan utvides ytterligere for å tillate argumentverdiene å være dynamiske, så lenge de oppfyller visse kriterier. Kanskje vi bare ønsker å sikre at en streng er overført til en metode:
$ Blindprøve-> shouldReceive ( 'get') -> med (Mockery :: type ( 'string')) -> én gang ();Eller kanskje argumentet må samsvare med et vanlig uttrykk. La oss påstå at ethvert filnavn som slutter med
.tekst
bør matches.$ mockedFile-> shouldReceive ('put') -> med ('/ \. txt $ /', Mockery :: any ()) -> once ();Og som et siste (men ikke begrenset til) eksempel, la oss tillate en rekke akseptable verdier ved å bruke
anyof
Matcher.$ mockedFile-> shouldReceive ('get') -> med (Mockery :: anyOf ('log.txt', 'cache.txt')) -> en gang ();Med denne koden vil forventningen bare gjelde hvis det første argumentet til
få
metoden erlog.txt
ellercache.txt
. Ellers kastes et Mockery-unntak når testene kjøres.Mockery \ Exception \ NoMatchingExpectationException: Ingen matchende handler funnet ...Tips: Ikke glem, du kan alltid alias
hån
somm
på toppen av klassen din for å gjøre ting litt mer kortfattet:bruk Mockery som m;
. Dette gjør det mulig for de mer kortfattede,m :: mock ()
.Til slutt har vi en rekke alternativer for å spesifisere hva mocked-metoden skal gjøre eller returnere. Kanskje trenger vi bare det til å returnere en boolsk. Lett:
$ mock-> shouldReceive ('method') -> once () -> ogReturn (false);
Delvis mocks
Du kan oppleve at det er situasjoner når du bare trenger å lytte til en enkelt metode, i stedet for hele objektet. La oss forestille oss, i dette eksemplet, at en metode på klassen din refererer til en tilpasset global funksjon (gisp) for å hente en verdi fra en konfigurasjonsfil.
getOption ( 'timeout'); // gjør noe med $ timeoutMens det er noen forskjellige teknikker for å mocking globale funksjoner. Ikke desto mindre er det best å unngå denne metoden samtale sammen. Dette er nettopp når partielle mocks kommer inn i spill.
offentlig funksjon testPartialMockExample () $ mock = Mockery :: mock ('MyClass [getOption]'); $ mock-> shouldReceive ('getOption') -> en gang () -> ogReturn (10000); $ Blindprøve-> brann ();Legg merke til hvordan vi har plassert metoden for å mocke innenfor parentes. Skulle du ha flere metoder, bare skille dem med et komma, slik som:
$ mock = Mockery :: mock ('MyClass [metode1, metode2]');Med denne teknikken vil resten av metodene på objektet utløse og oppføre seg som de normalt ville. Husk at du alltid må erklære oppførselen til dine mocked metoder, som vi har gjort ovenfor. I dette tilfellet, når
getOption
kalles, i stedet for å utføre koden i den, returnerer vi bare10000
.Et alternativt alternativ er å benytte passive partiske mocks, som du kan tenke på som å angi en standardstatus for mock objektet: alle metoder utsettes til hovedforeldreklassen, med mindre en forventning er spesifisert.
Den forrige kodebiten kan omskrives som:
offentlig funksjon testPassiveMockExample () $ mock = Mockery :: mock ('MyClass') -> makePartial (); $ mock-> shouldReceive ('getOption') -> en gang () -> ogReturn (10000); $ Blindprøve-> brann ();I dette eksemplet, alle metoder på
Klassen min
vil oppføre seg som de normalt ville, unntattgetOption
, som vil bli hånet og returnere 10000 '.
Hamcrest
Hamcrest-biblioteket tilbyr et ekstra sett av matchere for å definere forventninger.Når du har kjent deg med Mockery API, anbefales det at du også bruker Hamcrest-biblioteket, som gir et ekstra sett av matchere for å definere lesbare forventninger. Som Mockery, kan det bli installert gjennom Composer.
"require-dev": "hån / mockery": "dev-master", "davedevelopment / hamcrest-php": "dev-master"Når du har installert, kan du bruke en mer lesbar notasjon for å definere testene dine. Nedenfor finner du en rekke eksempler, inkludert små variasjoner som oppnår det samme sluttresultatet.
Legg merke til hvordan Hamcrest lar deg skrive dine påstander i så lesbar eller tvers som mulig. Bruken av
er()
funksjon er ikke noe mer enn syntaktisk sukker for å hjelpe til med lesbarhet.Du finner at Mockery passer ganske bra med Hamcrest. For eksempel, med Mockery alene, for å spesifisere at en mocked metode skal kalles med et enkelt argument av typen,
string
, du kan skrive:$ mock-> shouldReceive ('method') -> med (Mockery :: type ('string')) -> en gang ();Hvis du bruker Hamcrest,
Hån :: typen
Kan erstattes medstrengverdi()
, som så:$ mock-> shouldReceive ('method') -> med (stringValue ()) -> once ();Hamcrest følger ressursVerdi navnekonvensjon for å matche typen av en verdi.
nullValue
integerValue
arrayValue
Alternativt, for å matche ethvert argument, Hån :: noen ()
kan bli hva som helst()
.
$ file-> shouldReceive ('put') -> med ('foo.txt', noe ()) -> en gang ();
Den største hindringen for å bruke Mockery er ironisk nok ikke API, selv.
Den største hindringen for å bruke Mockery er ironisk nok ikke API, selv, men forstå hvorfor og når du skal bruke mocks i testingen din.
Nøkkelen er å lære og respektere enkeltansvarsprinsippet i din kodende arbeidsflyt. Coined av Bob Martin, dikterer SRP at en klasse "bør ha en, og bare en, grunn til å endre seg."Med andre ord, en klasse trenger ikke å bli oppdatert som svar på flere, uavhengige endringer i søknaden din, for eksempel endring av forretningslogikk, eller hvordan utdataene er formatert, eller hvordan dataene kan være vedvarende. I sin enkleste form, bare Som en metode, bør en klasse gjøre en ting.
De Fil
klassen administrerer filsysteminteraksjoner. EN MysqlDb
lagringsplass fortsetter data. en e-post
Klassen forbereder og sender e-post. Legg merke til hvordan i dette eksemplet ikke var ordet, og, Brukt.
Når dette er forstått, blir testen betydelig lettere. Dependensinjeksjon skal brukes til alle operasjoner som ikke faller under klassen paraply. Når du tester, fokuser på en klasse av gangen, og spot alle dets avhengigheter. Du er ikke interessert i å teste dem uansett; de har sine egne tester!
Selv om ingenting forhindrer deg i å bruke PHPUnits native mocking-implementering, hvorfor bry deg når Mockery's forbedrede lesbarhet er bare en komponistoppdatering
borte?