Alt om Mocking med PHPUnit

Det er to typer testing: "svart boks" og "hvit boks" stiler. Svart boks testing fokuserer på objektets tilstand; mens hvit boks testing fokuserer på atferd. De to stilene utfyller hverandre og kan kombineres til grundig testkode. Gjøre narr av tillater oss å teste oppførsel, og denne opplæringen kombinerer mocking konseptet med TDD for å bygge et eksempel klasse som bruker flere andre komponenter for å oppnå sitt mål.


Trinn 1: Introduksjon til atferdstesting

Objekter er enheter som sender meldinger til hverandre. Hvert objekt gjenkjenner et sett med meldinger som det igjen svarer til. Disse er offentlig metoder på et objekt. Privat metoder er nøyaktig motsatt. De er helt interne til en gjenstand og kan ikke kommunisere med noe utenfor objektet. Hvis offentlige metoder er relaterte til meldinger, så virker private metoder som tanker.

Summen av alle metoder, offentlige og private, tilgjengelige gjennom offentlige metoder, representerer oppførselen til et objekt. For eksempel, forteller et objekt til bevege seg forårsaker at det ikke bare er interaksjon med interne metoder, men også med andre objekter. Fra brukerens synspunkt har objektet bare en enkel oppførsel: den beveger seg.

Fra programmørens synspunkt må objektet imidlertid gjøre mange små ting for å oppnå bevegelsen.

For eksempel, tenk at vårt objekt er en bil. For at det skal bevege seg, Den må ha en løpende motor, være i det første giret (eller bakover), og hjulene må svinge. Dette er en oppførsel vi må teste og bygge på for å kunne designe og skrive produksjonskoden vår.


Trinn 2: Fjernstyrt lekebil

Vår testede klasse bruker aldri disse dummyobjektene.

La oss forestille oss at vi bygger et program for fjernkontroll av en leketøybil. Alle kommandoer til vår klasse kommer gjennom fjernkontrollen. Vi må lage en klasse som forstår hva fjernkontrollen sender og utsteder kommandoer til bilen.

Dette vil være et treningsprogram, og vi antar at de andre klassene som styrer de ulike delene av bilen allerede er skrevet. Vi vet nøyaktig signaturen til alle disse klassene, men dessverre kunne bilprodusenten ikke sende oss en prototype - ikke engang kildekoden. Alt vi vet er navnene på klassene, metodene de har, og hvilken oppførsel hver metode inkapsler. Returneringsverdiene er også angitt.


Trinn 3: Application Schema

Her er det komplette skjemaet for søknaden. Det er ingen forklaring på dette punktet; bare husk for senere referanse.


Trinn 4: Testdobler

En teststub er et objekt for å kontrollere den indirekte inngangen til testkoden.

Mocking er en teststype som krever sitt eget sett med verktøy, et sett med spesielle objekter som representerer forskjellige nivåer av faking oppførelsen av objektet. Disse er:

  • dummy objekter
  • teststubber
  • test spioner
  • test mocks
  • testfeil

Hver av disse objektene har sitt spesielle omfang og oppførsel. I PHPUnit er de opprettet med $ Dette-> getMock () metode. Forskjellen er hvordan og av hvilke grunner objektene blir brukt.

For bedre å forstå disse objektene, vil jeg implementere "Toy Car Controller" trinn for trinn ved hjelp av typer objekter, i rekkefølge, som nevnt ovenfor. Hvert objekt i listen er mer komplekst enn objektet før det. Dette fører til en implementering som er radikalt annerledes enn i den virkelige verden. Også, å være en imaginær applikasjon, vil jeg bruke noen scenarier som kanskje ikke engang er mulig i en ekte leketøybil. Men hei, la oss forestille oss hva vi trenger for å forstå det større bildet.


Trinn 5: Dummy Object

Dummy objekter er objekter som System Under Test (SUT) avhenger av, men de er faktisk aldri brukt. En dummyobjekt kan være et argument som sendes til et annet objekt, eller det kan returneres av et annet objekt og sendes videre til en tredje gjenstand. Poenget er at vår testede klasse aldri bruker disse dummyobjektene. Samtidig må objektet ligne et ekte objekt; Ellers kan mottakeren nekte det.

Den beste måten å eksemplifisere dette på er å forestille seg et scenario; skjemaet som er under:

Det oransje objektet er RemoteControlTranslator. Hovedformålet er å motta signaler fra fjernkontrollen og oversette dem til meldinger for våre klasser. På et tidspunkt vil brukeren gjøre en "Klar til å gå" handling på fjernkontrollen. Oversetteren vil motta meldingen og opprette klassene som er nødvendige for å gjøre bilen klar til å gå.

Produsenten sa det "Klar til å gå" betyr at motoren er startet, girkassen er i nøytral, og lysene er satt på eller av som per brukerforespørsel.

Dette betyr at brukeren kan forhåndsdefinere tilstanden til lysene før de er klare til å gå, og de vil slå på eller av basert på denne forhåndsdefinerte verdien ved aktivering. RemoteControlTranslator Sender all nødvendig informasjon til CarControl klasse' getReadyToGo ($ motor, $ girkasse, $ elektronikk, $ lys) metode. Jeg vet at dette er langt fra en perfekt design og bryter med noen få prinsipper og mønstre, men det er veldig bra for dette eksempelet.

Start vårt prosjekt med denne opprinnelige filstrukturen:

Husk, alle klassene i CarInterface mappen leveres av bilens produsent; Vi kjenner ikke til implementeringen. Alt vi vet er klassens signaturer, men vi bryr oss ikke om dem på dette punktet.

Vårt hovedmål er å implementere CarController klasse. For å teste denne klassen må vi forestille oss hvordan vi vil bruke den. Med andre ord setter vi oss selv i skoene til RemoteControlTranslator og / eller andre fremtidige klasser som kan bruke CarController. La oss begynne med å lage en sak for vår klasse.

klasse CarControllerTest utvider PHPUnit_Framework_TestCase 

Legg deretter til en testmetode.

 funksjon testItCanGetReadyTheCar () 

Nå tenk på hva vi må passere til getReadyToGo () Metode: En motor, en girkasse, en elektronikkstyring og lett informasjon. For dette eksemplets skyld vil vi bare spotte lysene:

require_once '... /CarController.php'; inkludere '... /autoloadCarInterfaces.php'; klasse CarControllerTest utvider PHPUnit_Framework_TestCase funksjon testItCanGetReadyTheCar () $ carController = ny CarController (); $ motor = ny motor (); $ girkasse = ny girkasse (); $ electornics = ny elektronikk (); $ dummyLights = $ this-> getMock ('Lights'); $ this-> assertTrue ($ carController-> getReadyToGo ($ motor, $ girkasse, $ electornics, $ dummyLights)); 

Dette vil tydeligvis mislykkes med:

PHP Fatal feil: Ring til udefinert metode CarController :: getReadyToGo ()

Til tross for feilen, ga denne testen oss et utgangspunkt for vår CarController gjennomføring. Jeg inkluderte en fil, kalt autoloadCarInterfaces.php, det var ikke på opprinnelig liste. Jeg skjønte at jeg trengte noe for å laste klassene, og jeg skrev en veldig grunnleggende løsning. Vi kan alltid skrive om det når de virkelige klassene er gitt, men det er en helt annen historie. For nå holder vi den enkle løsningen:

foreach (scandir (dirname (__ FILE__). '/ CarInterface') som $ filnavn) $ path = dirname (__ FILE__). '/ CarInterface /'. $ Filnavn; hvis (is_fil ($ path)) require_once $ path; 

Jeg antar at denne klasselasteren er åpenbar for alle; så, la oss diskutere testkoden.

Først oppretter vi en forekomst av CarController, klassen vi vil teste. Deretter oppretter vi forekomster av alle de andre klassene vi bryr oss om: motor, girkasse og elektronikk.

Vi lager da en dummy Lights objekt ved å ringe PHPUnit s getMock () metode og passerer navnet på Lights klasse. Dette returnerer en forekomst av Lights, men hver metode returnerer null--et dummyobjekt. Dette dummy objektet kan ikke gjøre noe, men det gir vår kode grensesnittet som er nødvendig for å jobbe med Lys objekter.

Det er veldig viktig å merke seg det $ dummyLights er en Lights objekt, og enhver bruker som forventer a Lys objekt kan bruke dummy objektet uten å vite at det ikke er ekte Lights gjenstand.

For å unngå forvirring anbefaler jeg at du angir en parameter type når du definerer en funksjon. Dette tvinger PHP-kjøretiden til å skrive inn argumentene som er overført til en funksjon. Uten å spesifisere datatypen, kan du sende ethvert objekt til en hvilken som helst parameter, noe som kan føre til feil i koden din. Med dette i tankene, la oss undersøke Elektronikk klasse:

require_once 'Lights.php'; klasse elektronikk funksjon turnOn (lys $ lys) 

La oss implementere en test:

klassen CarController funksjon getReadyToGo (motor $ motor, girkasse $ girkasse, elektronikk $ elektronikk, lys $ lys) $ engine-> start (); $ Gearbox-> Skift ( 'N'); $ Electronics-> turnon ($ lys); returnere sant; 

Som du kan se, er getReadyToGo () funksjonen brukes $ lys objekt for det eneste formålet med å sende det til $ elektronikk objektets Slå på() metode. Er dette den ideelle løsningen for en slik situasjon? Sannsynligvis ikke, men du kan tydelig observere hvordan en dummy objekt, uten noe forhold til getReadyToGo () funksjon, går videre til det ene objektet som virkelig trenger det.

Vær oppmerksom på at alle klassene i CarInterface katalog gir dummyobjekter når de initialiseres. Anta også at vi for denne øvelsen forventer at produsenten skal gi de virkelige klassene i fremtiden. Vi kan ikke stole på deres nåværende mangel på funksjonalitet; så må vi sørge for at våre tester passerer.


Trinn 6: "Stub" Status og Gå fremover

En teststub er et objekt for å kontrollere den indirekte inngangen til testkoden. Men hva er indirekte innspill? Det er en kilde til informasjon som ikke kan spesifiseres direkte.

Det vanligste eksempelet på en teststub er når en gjenstand ber om et annet objekt for informasjon, og gjør deretter noe med dataene.

Spioner, per definisjon, er mer dyktige stubber.

Dataene kan kun oppnås ved å spørre en bestemt gjenstand for det, og i mange tilfeller brukes disse objektene til et bestemt formål i den testede klassen. Vi ønsker ikke å "nye opp" (ny SomeClass ()) en klasse inne i en annen klasse for testformål. Derfor må vi injisere en forekomst av en klasse som virker som SomeClass uten å injisere en faktisk SomeClass gjenstand.

Det vi ønsker er en stubklasse, som da fører til avhengighetsinjeksjon. Dependency injection (DI) er en teknikk som injiserer en gjenstand i et annet objekt, og tvinger den til å bruke den injiserte gjenstanden. DI er vanlig i TDD, og ​​det er absolutt nødvendig i nesten alle prosjekter. Det gir en enkel måte å tvinge et objekt på å bruke en testklargjort klasse i stedet for en ekte klasse som brukes i produksjonsmiljøet.

La oss få vår leketøybil gå videre.

Vi ønsker å implementere en metode som kalles gå fremover(). Denne metoden spør først a StatusPanel Motta for drivstoff og motorstatus. Hvis bilen er klar til å gå, instruerer metoden at elektronikken skal akselerere.

For bedre å forstå hvordan en stub fungerer, vil jeg først skrive koden for statuskontroll og akselerasjon:

 funksjon goForward (Elektronikk $ elektronikk) $ statusPanel = new StatusPanel (); hvis ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ elektronikk-> akselerere (); 

Denne koden er ganske enkel, men vi har ikke en ekte motor eller drivstoff for å teste vår Gå framover() gjennomføring. Vår kode vil ikke engang komme inn i hvis uttalelse fordi vi ikke har en StatusPanel klasse. Men hvis vi fortsetter med testen, begynner en logisk løsning å dukke opp:

 funksjon testItCanAccelerate () $ carController = ny CarController (); $ elektronikk = ny elektronikk (); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ StubStatusPanel-> forventer ($ dette-> en hvilken som helst ()) -> metode ( 'thereIsEnoughFuel') -> vil ($ dette-> return (SANN)); $ StubStatusPanel-> forventer ($ dette-> en hvilken som helst ()) -> metode ( 'engineIsRunning') -> vil ($ dette-> return (SANN)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Linje for linje forklaring:

Jeg elsker rekursjon; Det er alltid lettere å teste rekursjon enn løkker.

  • lage en ny CarController
  • opprett den avhengige Elektronikk gjenstand
  • skape en mock for StatusPanel
  • Forvent å ringe thereIsEnoughFuel () null eller flere ganger og tilbake ekte
  • Forvent å ringe engineIsRunning () null eller flere ganger og tilbake ekte
  • anrop Gå framover() med Elektronikk og StubbedStatusPanel gjenstand

Dette er testen vi vil skrive, men det vil ikke fungere med vår nåværende implementering av Gå framover(). Vi må endre det:

 funksjon goForward (Elektronikk $ elektronikk, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : Ny StatusPanel (); hvis ($ statusPanel-> engineIsRunning () && $ statusPanel-> thereIsEnoughFuel ()) $ elektronikk-> akselerere (); 

Vår modifikasjon bruker avhengighetsinjeksjon ved å legge til en ekstra valgfri parameter av typen StatusPanel. Vi avgjør om denne parameteren har en verdi og opprett en ny StatusPanel hvis $ statusPanel er null. Dette sikrer at en ny StatusPanel objekt er opprettet i produksjon, samtidig som vi tillater oss å teste metoden.

Det er viktig å spesifisere typen av $ statusPanel parameter. Dette sikrer at bare a StatusPanel objekt (eller et objekt av en arvet klasse) kan overføres til metoden. Men selv med denne modifikasjonen er vår test fortsatt ikke fullført.


Trinn 7: Fullfør testen med en ekte testmock

Vi må teste mock an Elektronikk protester mot at vår metode fra trinn 6 ringer akselerere(). Vi kan ikke bruke den virkelige Elektronikk klassen av flere grunner:

  • Vi har ikke klassen.
  • Vi kan ikke bekrefte atferden.
  • Selv om vi kunne kalle det, bør vi teste det i isolasjon.

En testmock er et objekt som er i stand til å kontrollere både indirekte inngang og utgang, og den har en mekanisme for automatisk påstand om forventninger og resultater. Denne definisjonen kan høres litt forvirrende, men det er egentlig ganske enkelt å implementere:

 funksjon testItCanAccelerate () $ carController = ny CarController (); $ electronics = $ this-> getMock ('Electronics'); $ Electronics-> forventer ($ dette-> én gang ()) -> metode ( 'akselerere'); $ stubStatusPanel = $ this-> getMock ('StatusPanel'); $ StubStatusPanel-> forventer ($ dette-> en hvilken som helst ()) -> metode ( 'thereIsEnoughFuel') -> vil ($ dette-> return (SANN)); $ StubStatusPanel-> forventer ($ dette-> en hvilken som helst ()) -> metode ( 'engineIsRunning') -> vil ($ dette-> return (SANN)); $ carController-> goForward ($ electronics, $ stubStatusPanel); 

Vi endret bare $ elektronikk variabel. I stedet for å skape en ekte Elektronikk objekt, vi bare spotte en.

På neste linje, definerer vi en forventning på $ elektronikk gjenstand. Mer presist, vi forventer at akselerere() Metoden kalles bare en gang ($ Dette-> én gang ()). Testen går nå!

Ta gjerne med deg med denne testen. Prøv å endre $ Dette-> én gang () inn i $ Dette-> nøyaktig (2) og se hva en fin feilmelding PHPUnit gir deg:

1) CarControllerTest :: testItCanAccelerate Forventning mislyktes for metode navn er lik ; når påkalt 2 gang (er). Metoden ble forventet å bli kalt 2 ganger, faktisk kalt 1 ganger.

Trinn 8: Bruk en testspion

En testspion er et objekt som er i stand til å fange indirekte utdata og gir indirekte innspill etter behov.

Indirekte utdata er noe vi ikke direkte kan observere. For eksempel: Når den testede klassen beregner en verdi, og bruker den som et argument for en annen objekts metode. Den eneste måten å observere denne utgangen på er å spørre det oppkalte objektet om variabelen som brukes til å få tilgang til metoden.

Denne definisjonen gjør en spion nesten en mock.

Hovedforskjellen mellom en mock og spion er at mock objekter har innebygde påstander og forventninger.

I så fall, hvordan kan vi opprette en testspion ved hjelp av PHPUnits getMock ()? Vi kan ikke (vel, vi kan ikke skape en ren spion), men vi kan lage mocks som er i stand til å spionere andre gjenstander.

La oss implementere bremsesystemet slik at vi kan stoppe bilen. Bremsing er veldig enkel; fjernkontrollen vil registrere bremseintensiteten fra brukeren og sende den til kontrolleren. Fjernkontrollen gir også et "Nødstopp!" knapp. Dette må øyeblikkelig koble inn bremser med maksimal effekt.

Bremsekraften måler verdier fra 0 til 100, med 0 betyr ingenting og 100 betyr maksimal bremsekraft. "Nødstopp!" kommandoen vil bli mottatt som en annen samtale.

De CarController vil utstede en melding til Elektronikk gjenstand for å aktivere bremsesystemet. Bilkontrollanten kan også spørre StatusPanel for hastighetsinformasjon oppnådd gjennom sensorer på bilen.

Gjennomføring Ved hjelp av en ren testspion

La oss først implementere et rent spionobjekt uten å bruke PHPUnits mocking-infrastruktur. Dette vil gi deg en bedre forståelse av test spion konseptet. Vi starter med å sjekke Elektronikk objektets underskrift.

Klasse Elektronikk funksjon turnOn (Lys $ lys)  funksjon akselerere ()  funksjon pushBrakes ($ brakingPower) 

Vi er interessert i pushBrakes () metode. Jeg kalte det ikke brems() for å unngå forvirring med gå i stykker søkeord i PHP.

For å skape en ekte spion, vil vi utvide Elektronikk og tilsidesatte pushBrakes () metode. Denne overstyrte metoden vil ikke skyve bremsen; I stedet vil det bare registrere bremsekraften.

klasse SpyingElectronics utvider elektronikk privat $ brakingPower; funksjon pushBrakes ($ brakingPower) $ this-> brakingPower = $ brakingPower;  funksjon getBrakingPower () return $ this-> brakingPower; 

Den getBrakingPower () Metoden gir oss muligheten til å kontrollere bremsekraften i vår test. Dette er ikke en metode vi vil bruke i produksjonen.

Vi kan nå skrive en test som er i stand til å teste bremsekraften. Etter TDD-prinsippene starter vi med den enkleste testen og gir den mest grunnleggende implementeringen:

 funksjon testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = ny SpyingElectronics (); $ carController = ny CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); 

Denne testen feiler fordi vi ikke har en pushBrakes () metode på CarController. La oss rette ut det og skrive en:

 funksjon pushBrakes ($ brakingPower, Electronics $ electronics) $ electronics-> pushBrakes ($ brakingPower); 

Testen går nå, effektivt testing av pushBrakes () metode.

Vi kan også spionere på metallsamtaler. Testing av StatusPanel klassen er det neste logiske trinnet. Det gir brukeren forskjellige opplysninger om den fjernstyrte bilen. La oss skrive en test som kontrollerer om StatusPanel Objektet blir spurt om bilens fart. Vi lager et spion for det:

klasse SpyingStatusPanel utvider StatusPanel private $ speedWasRequested = false; funksjon getSpeed ​​() $ this-> speedWasRequested = true;  funksjonshastighetWasRequested () return $ this-> speedWasRequested; 

Deretter endrer vi testen vår for å bruke spionen:

 funksjon testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = ny SpyingElectronics (); $ statusPanelSpy = ny SpyingStatusPanel (); $ carController = ny CarController (); $ carController-> pushBrakes ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ Dette-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); 

Merk at jeg ikke skrev en egen test.

Anbefalingen av "en påstand per test" er god å følge, men når testen beskriver en handling som krever flere trinn eller tilstander, er bruk av mer enn én påstand i samme test akseptabel.

Enda mer, dette holder dine påstander om et enkelt konsept på ett sted. Dette bidrar til å eliminere duplikatkode ved ikke å kreve at du gjentatte ganger oppretter de samme forholdene for din SUT.

Og nå gjennomføringen:

 funksjon pushBrakes ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : Ny StatusPanel (); $ Electronics-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Det er bare en liten, liten ting som plager meg: navnet på denne testen er testItCanStop (). Det innebærer tydeligvis at vi skyver bremsene til bilen kommer til et helt stopp. Vi kalte imidlertid metoden pushBrakes (), som ikke er helt riktig. Tid til refactor:

 funksjonsstopp ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : Ny StatusPanel (); $ Electronics-> pushBrakes ($ brakingPower); $ StatusPanel-> getSpeed ​​(); 

Ikke glem å endre metodeanropet i testen også.

$ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy);

Indirekte utdata er noe vi ikke direkte kan observere.

På dette tidspunktet må vi tenke på vårt bremsesystem og hvordan det fungerer. Det er flere muligheter, men for dette eksempelet anta at leverandøren av leketøybilen angav at bremsing skjer i diskrete intervaller. Ringer en Elektronikk objektets pushBreakes () Metoden skyver bremsen i en diskret tid og frigjør den deretter. Tidsintervallet er ubetydelig for oss, men la oss forestille oss at det er en brøkdel av et sekund. Med et så lite tidsintervall må vi kontinuerlig sende pushBrakes () Kommandoer til hastigheten er null.

Spioner, per definisjon, er mer dyktige stubber, og de kan også kontrollere indirekte inngang hvis det er nødvendig. La oss lage vår StatusPanel Spion mer dyktig og gir litt verdi for hastigheten. Jeg tror den første samtalen skal gi en positiv hastighet - la oss si verdien av 1. Den andre anropet vil gi hastigheten til 0.

klasse SpyingStatusPanel utvider StatusPanel private $ speedWasRequested = false; privat $ currentSpeed ​​= 1; funksjon getSpeed ​​() if ($ this-> speedWasRequested) $ this-> currentSpeed ​​= 0; $ this-> speedWasRequested = true; returnere $ this-> currentSpeed;  funksjonshastighetWasRequested () return $ this-> speedWasRequested;  funksjon spyOnSpeed ​​() return $ this-> currentSpeed; 

Den overstyrte getSpeed ​​() Metoden returnerer riktig hastighetsverdi via spyOnSpeed ​​() metode. La oss legge til et tredje påstand om vår test:

 funksjon testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = ny SpyingElectronics (); $ statusPanelSpy = ny SpyingStatusPanel (); $ carController = ny CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); $ this-> assertEquals ($ halfBrakingPower, $ electronicsSpy-> getBrakingPower ()); $ Dette-> assertTrue ($ statusPanelSpy-> speedWasRequested ()); $ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​()); 

Ifølge den siste påstanden, bør hastigheten ha en hastighetsverdi på 0 etter Stoppe() Metoden fullfører utførelse. Kjører denne testen mot vår produksjonskode resulterer i en feil med en kryptisk melding:

1) CarControllerTest :: testItCanStop Feilet hevdet at 1 matcher forventet 0.

La oss legge til vår egen e-postmelding:

$ this-> assertEquals (0, $ statusPanelSpy-> spyOnSpeed ​​(), 'Forventet hastighet å være 0 (null) etter å ha stoppet, men det var faktisk'. $ statusPanelSpy-> spyOnSpeed ​​());

Det gir en mye mer lesbar feilmelding:

1) CarControllerTest :: testItCanStop Forventet hastighet til å være 0 (null) etter å ha stoppet, men det var faktisk 1 Mislyktes hevdet at 1 matcher forventet 0.

Nok feil! La oss få det til å passere.

 funksjonsstopp ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : Ny StatusPanel (); $ Electronics-> pushBrakes ($ brakingPower); hvis ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Jeg elsker rekursjon; Det er alltid lettere å teste rekursjon enn løkker. Lettere testing betyr enklere kode, som igjen betyr en bedre algoritme. Ta en titt på The Transformation Priority Premise for mer om dette emnet.

Komme tilbake til PHPUnits Mocking Framework

Nok med de ekstra klassene. La oss omskrive dette ved hjelp av PHPUnits mocking-rammeverk og eliminere de rene spionene. Hvorfor?

Fordi PHPUnit tilbyr bedre og enklere mocking syntaks, mindre kode, og noen fine forhåndsdefinerte metoder.

Jeg oppretter vanligvis bare rene spioner og stubber når de mocking dem med getMock () ville være for komplisert. Hvis klassene dine er så komplekse det getMock () kan ikke håndtere dem, da har du et problem med produksjonskoden din - ikke med deg tester.

 funksjon testItCanStop () $ halfBrakingPower = 50; $ electronicsSpy = $ this-> getMock ('Electronics'); $ ElectronicsSpy-> forventer ($ dette-> nøyaktig (2)) -> Metode ( 'pushBrakes') -> med ($ halfBrakingPower); $ statusPanelSpy = $ this-> getMock ('StatusPanel'); $ StatusPanelSpy-> forventer ($ dette-> i (0)) -> metode ( 'getSpeed') -> vil ($ dette-> return (1)); $ StatusPanelSpy-> forventer ($ dette-> i (1)) -> metode ( 'getSpeed') -> vil ($ dette-> return (0)); $ carController = ny CarController (); $ carController-> stop ($ halfBrakingPower, $ electronicsSpy, $ statusPanelSpy); 

Summen av alle metoder, offentlige og private, tilgjengelige gjennom offentlige metoder, representerer oppførselen til et objekt.

En linje for linje forklaring av ovennevnte kode:

  • sett halv bremsekraft = 50
  • opprett en Elektronikk håne
  • Forvent metode pushBrakes () å utføre nøyaktig to ganger med den ovenfor angitte bremsekraften
  • lage en StatusPanel håne
  • komme tilbake 1 på først getSpeed ​​() anrop
  • komme tilbake 0 på andre getSpeed ​​() henrettelse
  • ring den testede Stoppe() metode på en ekte CarController gjenstand

Sannsynligvis den mest interessante tingen i denne koden er $ Dette-> på ($ someValue) metode. PHPUnit teller mengden samtaler til det mock. Counting skjer på spotnivået; så, ring flere metoder på $ statusPanelSpy ville øke disken. Dette kan virke litt motintuitivt i begynnelsen; så la oss se på et eksempel.

Anta at vi ønsker å sjekke drivstoffnivået på hver samtale til Stoppe(). Koden vil se slik ut:

 funksjonsstopp ($ brakingPower, Electronics $ electronics, StatusPanel $ statusPanel = null) $ statusPanel = $ statusPanel? : Ny StatusPanel (); $ Electronics-> pushBrakes ($ brakingPower); $ StatusPanel-> thereIsEnoughFuel (); hvis ($ statusPanel-> getSpeed ​​()) $ this-> stop ($ brakingPower, $ electronics, $ statusPanel); 

Dette vil bryte vår test. Du kan være forvirret hvorfor, men du får følgende melding:

1) CarControllerTest :: testItCanStop Forventning mislyktes for metodenavnet er lik  når påkalt 2 gang (er). Metoden ble forventet å bli kalt 2 ganger, faktisk kalt 1 ganger.

Det er ganske åpenbart at pushBrakes () bør kalles to ganger. Hvorfor mottar vi denne meldingen? På grunn av $ Dette-> på ($ someValue) forventning. Telleren øker som følger:

  • første anrop til Stoppe() -> første anrop til thereIsEnougFuel () => intern teller på 0
  • første anrop til Stoppe() -> første anrop til getSpeed ​​() => intern teller ved 1 og komme tilbake 0
  • andre anrop til Stoppe() skjer aldri => andre anrop til getSpeed ​​() skjer aldri

Hver samtale til noen mocked metode på $ statusPanelSpy øker PHPUnits interne teller.


Trinn 9: En testfake

Hvis offentlige metoder er relaterte til meldinger, så virker private metoder som tanker.

En testfake er en enklere implementering av et produksjonskodeobjekt. Dette er en veldig lignende definisjon for teststubber. I virkeligheten er Fakes and Stubs svært like som per ekstern oppførsel. Begge er objekter som etterligner oppførselen til noen andre virkelige objekter, og begge implementerer en metode for å kontrollere indirekte inngang. Forskjellen er at Fakes er mye mer nærmere et ekte objekt enn til et dummyobjekt.

En stub er i utgangspunktet et dummyobjekt hvis metoder returnerer forhåndsdefinerte verdier. En falsk gjør imidlertid en fullstendig implementering av et reelt objekt på en mye enklere måte. Sannsynligvis er det vanligste eksemplet en InMemoryDatabase å perfekt simulere en ekte databaseklasse uten å skrive til datalageret. Dermed blir testingen raskere.

Testfakes bør ikke implementere noen metoder for direkte kontroll av inngang eller retur observerbar tilstand. De er ikke vant til å bli utspurt; de er vant til å gi - ikke observere. De vanligste brukssakene til Fakes er når den virkelige avhengigheten av komponent (DOC) ennå ikke er skrevet, den er for sakte (som en database), eller den virkelige DOC er ikke tilgjengelig i testmiljøet.


Trinn 10: Konklusjoner

Den viktigste mock funksjonaliteten er å kontrollere DOC. Det gir også en fin måte å kontrollere indirekte I / O ved hjelp av avhengighetsinjeksjonsteknikker.

Det er to hoved meninger om mocking:

Noen sier at mocking er dårlig ...

  • Noen sier det mocking er dårlig, og de har rett. Mocking gjør noe subtilt og styggt: det binder for mye av testene til implementeringen. Når det er mulig, bør testen være så uavhengig av implementeringen som mulig. Svart boks testing er alltid å foretrekke for hvit boks testing. Test alltid tilstanden hvis du kan; ikke tøffe oppførsel. Å være anti-mockist oppfordrer bunn-up utvikling og design. Dette betyr at de små komponentdelene av systemet er opprettet først og deretter kombinert til en harmonisk struktur.
  • Noen sier det mocking er bra, og de har rett. Mocking gjør noe subtilt og vak