Testing er grunnlaget for solid programvareutvikling. Det er mange typer testing, men den viktigste typen er enhetstesting. Enhetstesting gir deg mye tillit til at du kan bruke velprøvde stykker som primitiver og stole på dem når du komponerer dem for å lage ditt program. De øker din beholdning av klarert kode utover språkets builtins og standardbibliotek. I tillegg gir Python stor støtte til å skrive enhetstester.
Før du drar inn i alle prinsippene, heuristikk og retningslinjer, la vi se en representativ enhetstest i aksjon. De SelfDrivingCar
Klassen er en delvis implementering av kjørelogikken til en selvkjørende bil. Det handler mest om å kontrollere bilens hastighet. Det er oppmerksom på objekter foran det, fartsgrensen, og om det kommer til bestemmelsesstedet eller ej.
klasse SelfDrivingCar (objekt): def __init __ (selv): self.speed = 0 self.destination = Ingen def _accelerate (self): self.speed + = 1 def _decelerate (self): hvis self.speed> 0: self.speed - = 1 def _advance_to_destination (self): avstand = self._calculate_distance_to_object_in_front () hvis avstand < 10: self.stop() elif distance < self.speed / 2: self._decelerate() elif self.speed < self._get_speed_limit(): self._accelerate() def _has_arrived(self): pass def _calculate_distance_to_object_in_front(self): pass def _get_speed_limit(self): pass def stop(self): self.speed = 0 def drive(self, destination): self.destination = destination while not self._has_arrived(): self._advance_to_destination() self.stop() def __init__(self): self.speed = 0 self.destination = None def _accelerate(self): self.speed += 1 def _decelerate(self): if self.speed > 0: self.speed - = 1 def _advance_to_destination (selv): avstand = self._calculate_distance_to_object_in_front () hvis avstand < 10: self.stop() elif distance < self.speed / 2: self._decelerate() elif self.speed < self._get_speed_limit(): self._accelerate() def _has_arrived(self): pass def _calculate_distance_to_object_in_front(self): pass def _get_speed_limit(self): pass def stop(self): self.speed = 0 def drive(self, destination): self.destination = destination while not self._has_arrived(): self._advance_to_destination() self.stop()
Her er en enhetstest for Stoppe()
metode for å appetitten din. Jeg kommer inn i detaljene senere.
fra selvsagt import TestCase klasse SelfDrivingCarTest (TestCase): def setUp (selv): self.car = SelfDrivingCar () def test_stop (selv): self.car.speed = 5 self.car.stop () # Bekreft at hastigheten er 0 etter stopper self.assertEqual (0, self.car.speed) # Bekreft at det er Ok å stoppe igjen hvis bilen allerede er stoppet selv.car.stop () self.assertEqual (0, self.car.speed)
Å skrive gode enhetstester er hardt arbeid. Skriveverkstester tar tid. Når du gjør endringer i koden din, må du også endre testene dine også. Noen ganger har du feil i testkoden din. Det betyr at du må være virkelig forpliktet. Fordelene er enorme, selv for små prosjekter, men de er ikke gratis.
Du må være disiplinert. Være konsekvent. Pass på at testene alltid passerer. Ikke la testene bli ødelagt fordi du "vet" at koden er OK.
For å hjelpe deg å være disiplinert, bør du automatisere enhetstester. Tester skal kjøre automatisk på viktige punkter som forplikter eller pre-distribusjon. Ideelt sett avviser kildekontrollhåndteringssystemet kode som ikke bestod alle sine tester.
Hvis du ikke testet det, kan du ikke si at det fungerer. Dette betyr at du bør vurdere det ødelagt. Hvis det er kritisk kode, må du ikke distribuere den til produksjon.
En enhet for enhetstesting er en fil / modul som inneholder et sett med relaterte funksjoner eller en klasse. Hvis du har en fil med flere klasser, bør du skrive en enhetstest for hver klasse.
Testdrevet utvikling er en praksis der du skriver testene før du skriver koden. Det er flere fordeler med denne tilnærmingen, men jeg anbefaler å unngå det hvis du har disiplinen til å skrive riktig tester senere.
Årsaken er at jeg designer med kode. Jeg skriver kode, ser på det, skriver om det, ser på det igjen og skriver om det igjen veldig raskt. Skriveprøver begrenser meg først og bremser meg.
Når jeg er ferdig med det opprinnelige designet, skriver jeg testene umiddelbart, før de integreres med resten av systemet. Når det er sagt, er det en fin måte å introdusere deg på enhetstester, og det sikrer at hele koden din vil ha tester.
Den unittest modulen leveres med Pythons standardbibliotek. Det gir en klasse kalt Testforsøk
, som du kan utlede klassen din fra. Deretter kan du overstyre a SETUP ()
metode for å utarbeide en testfixtur før hver test og / eller a classSetUp ()
klassemetode for å utarbeide en testfesting for alle tester (ikke tilbakestilt mellom individuelle tester). Det er tilsvarende rive ned()
og classTearDown ()
metoder du kan overstyre også.
Her er de relevante delene fra vår SelfDrivingCarTest
klasse. Jeg bruker bare SETUP ()
metode. Jeg lager en frisk SelfDrivingCar
instans og lagre den i self.car
så det er tilgjengelig for hver test.
fra unittest import TestCase klasse SelfDrivingCarTest (TestCase): def setUp (selv): self.car = SelfDrivingCar ()
Det neste trinnet er å skrive spesifikke testmetoder for å teste den koden under test-the SelfDrivingCar
klasse i dette tilfellet-gjør hva den skal gjøre. Strukturen til en testmetode er ganske vanlig:
Vær oppmerksom på at resultatet ikke må være utdata fra en metode. Det kan være en tilstandsendring av en klasse, en bivirkning som å legge til en ny rad i en database, skrive en fil eller sende en e-post.
For eksempel, Stoppe()
metode av SelfDrivingCar
klassen returnerer ikke noe, men det endrer den interne tilstanden ved å sette hastigheten til 0. Den assertEqual ()
metode gitt av Testforsøk
baseklasse brukes her for å bekrefte det ringer Stoppe()
jobbet som forventet.
def test_stop (selv): self.car.speed = 5 self.car.stop () # Bekreft at hastigheten er 0 etter å ha stoppet self.assertEqual (0, self.car.speed) # Bekreft at det er Ok å stoppe igjen hvis bilen er allerede stanset selv.car.stop () self.assertEqual (0, self.car.speed)
Det er faktisk to tester her. Den første testen er å sørge for at hvis bilens fart er 5 og Stoppe()
blir kalt, så blir hastigheten 0. Deretter er en annen test for å sikre at ingenting går galt hvis du ringer Stoppe()
igjen når bilen allerede er stoppet.
Senere vil jeg introdusere flere tester for ekstra funksjonalitet.
Doktormodulen er ganske interessant. Den lar deg bruke interaktive kodeprøver i dokumentasjonen din og bekrefte resultatene, inkludert hevede unntak.
Jeg bruker eller anbefaler ikke doktest for store systemer. Riktig enhetstesting tar mye arbeid. Testkoden er vanligvis mye større enn koden under testen. Docstrings er bare ikke det rette mediumet for å skrive omfattende tester. De er kule, skjønt. Her er hva en fakultet
funksjon med doc tester ser ut som:
Importer matematisk def-faktor (n): "" "Returner faktorial av n, et nøyaktig heltall> = 0. Hvis resultatet er lite nok til å passe inn en int, returner en int. Else returnerer en lang. >>> [factorial (n) for n i rekkevidde (6)] [1, 1, 2, 6, 24, 120] >>> [faktorial (lang (n)) for n i rekkevidde (6)] [1, 1, 2, 6, 24, 120] >>> factorial (30) 265252859812191058636308480000000L >>> factorial (30L) 265252859812191058636308480000000L >>> factorial (-1) Traceback (siste samtale siste): ... ValueError: n må være> = 0 Fakta om flyter OK, men float må være et nøyaktig heltall: >>> factorial (30.1) Traceback (siste samtalen sist): ... ValueError: n må være nøyaktig heltall >>> factorial (30.0) 265252859812191058636308480000000L Det må heller ikke være latterlig stort : >>> factorial (1e100) Traceback (siste anrop sist): ... OverflowError: n for stor "" "hvis ikke n> = 0: heve ValueError (" n må være = 0 ") hvis math.floor )! = n: heve ValueError ("n må være nøyaktig heltall") hvis n + 1 == n: # ta en verdi som 1e300 heve OverflowE rror ("for stor") resultat = 1 faktor = 2 mens faktor <= n: result *= factor factor += 1 return result if __name__ == "__main__": import doctest doctest.testmod()
Som du kan se er docstring mye større enn funksjonskoden. Det fremmer ikke lesbarhet.
OK. Du skrev din enhetstester. For et stort system, har du titalls / hundrevis / tusenvis av moduler og klasser på tvers av muligens flere kataloger. Hvordan kjører du alle disse testene?
Den unittest modulen gir ulike muligheter til å gruppere tester og kjøre dem programmatisk. Sjekk ut lasting og kjøringstester. Men den enkleste måten er testfunn. Dette alternativet ble bare lagt til i Python 2.7. Pre-2.7 kan du bruke nese til å oppdage og kjøre tester. Nese har noen andre fordeler som å kjøre testfunksjoner uten å måtte lage en klasse for testfasen. Men for formålet med denne artikkelen, la oss holde fast med unittest.
For å oppdage og kjøre dine unittest-baserte tester, skriv bare på kommandolinjen:
python -m unittest oppdage
unittest vil skanne alle filene og underkatalogene, kjøre eventuelle tester den finner, og gi en fin rapport, så vel som runtime. Hvis du vil se hvilke tester det kjører, kan du legge til -v-flagget:
python -m unittest discover -v
Det er flere flagg som kontrollerer operasjonen:
python -m unittest -h Bruk: python -m unittest [alternativer] [tester] Alternativer: -h, --help Vis denne meldingen -v, --verbose Verbose output -q, --quiet Minimal utgang -f, - failfast Stopp på første feil -c, --atch Catch control-C og vis resultater -b, - buffer Buffer stdout og stderr under testkjøringer Eksempler: python -m unittest test_module - kjør tester fra test_module python -m unittest module.TestClass - Kjør tester fra modul.TestClass python -m unittest module.Class.test_method - Kjør spesifisert testmetode [tester] kan være en liste over et antall testmoduler, klasser og testmetoder. Alternativ bruk: python -m unittest discover [alternativer] Alternativer: -v, --verbose Verbose output -f, --failfast Stopp ved første feil -c, --atch Fangekontroll-C og vis resultater -b, --buffer Buffer stdout og stderr under testkjøringer -s directory Directory for å starte oppdagelsen ('.' Standard) -p mønster Mønster for å matche testfiler ('test * .py' standard) -t katalog Toppnivåkatalog for prosjekt (standard for å starte katalogen ) For testfunn må alle testmoduler importeres fra prosjektets toppnivåkatalog.
Testdekning er et ofte forsømt felt. Dekning betyr hvor mye koden din faktisk er testet av testene dine. For eksempel, hvis du har en funksjon med en if-else
uttalelse og du tester bare hvis
gren, så vet du ikke om ellers
grenverk eller ikke. I følgende kodeeksempel, funksjonen Legg til()
sjekker typen av argumenter. Hvis begge er heltall, legges det bare til dem.
Hvis begge er strenger, prøver den å konvertere dem til heltall og legger til dem. Ellers reiser det et unntak. De test_add ()
funksjonstester Legg til()
fungere med argumenter som er både heltall og med argumenter som flyter og verifiserer den rette oppførselen i hvert tilfelle. Men testdekning er ufullstendig. Saken av strengargumenter ble ikke testet. Som et resultat går testen vellykket, men skrivefeltet i grenen der argumentene er begge strenger, ble ikke oppdaget (se "intg" der?).
importere unittest def add (a, b): "" "Denne funksjonen legger til to tall a, b og returnerer summen a og b kan heltall" "" hvis isinstans (a, int) og isinstance (b, int) + b elseif isinstance (a, str) og isinstance (b, str): retur int (a) + intg (b) annet: heve Unntak ('Ugyldige argumenter') klasse Test (unittest.TestCase): def test_add : self.assertEqual (5, add (2, 3)) self.assertEqual (15, add (-6, 21)) self.assertRaises (Unntak, legg til, 4,0, 5,0) unittest.main ()
Her er utgangen:
---------------------------------------------------------------------- Ran 1 test i 0.000s OK Prosess ferdig med utgangskode 0
Det er ikke enkelt eller enkelt å skrive industrielle styrkenhetstester. Det er flere ting å vurdere og trade-offs å bli gjort.
Hvis koden din er det som kalles formelt spaghetti kode eller en stor ball av gjørme hvor forskjellige nivåer av abstraksjon blandes sammen og hvert stykke kode avhenger av hvert annet stykke kode, har du det vanskelig å teste det. Også, når du endrer noe, må du også oppdatere en mengde tester.
Den gode nyheten er at generell hensiktsmessig programvaredesign er akkurat det du trenger for testbarhet. Spesielt velmodifisert modulær kode, der hver komponent har klart ansvar og samhandler med andre komponenter via veldefinerte grensesnitt, vil gjøre skrive gode enhetstester en fornøyelse.
For eksempel, vår SelfDrivingCar
klassen er ansvarlig for høyt nivå drift av bilen: gå, stopp, navigere. Den har en calculate_distance_to_object_in_front ()
Metode som ikke er implementert ennå. Denne funksjonaliteten skal trolig bli implementert av et helt eget delsystem. Det kan inkludere lesedata fra forskjellige sensorer, interagere med andre selvkjørende biler, en hel maskinvisningsstabel for å analysere bilder fra flere kameraer.
La oss se hvordan dette fungerer i praksis. De SelfDrivingCar
vil akseptere et argument som heter object_detector
som har en metode som kalles calculate_distance_to_object_in_front ()
, og det vil delegere denne funksjonaliteten til dette objektet. Nå er det ikke nødvendig å prøve enheten på grunn av dette object_detector
er ansvarlig (og bør testes) for det. Du vil fortsatt prøve å teste det faktum at du bruker object_detector
riktig.
klasse SelfDrivingCar (objekt): def __init __ (self, object_detector): self.object_detector self.speed = 0 self.destination = Ingen def _calculate_distance_to_object_in_front (selv): return self.object_detector.calculate_distance_to_object_in_front ()
Mengden innsats du legger inn i testing, bør korreleres til kostnadene ved feil, hvor stabil koden er, og hvor lett det er å fikse hvis det oppdages problemer nedover linjen.
For eksempel er vår selvkørende bilklasse superkritisk. Hvis Stoppe()
Metoden virker ikke riktig, vår bil som kjører selv kan drepe folk, ødelegge eiendommen og spore hele det selvbilde bilmarkedet. Hvis du utvikler en selvkjørende bil, mistenker jeg at enheten din tester for Stoppe()
Metoden vil være litt strengere enn min.
På den annen side, hvis en enkelt knapp i nett applikasjon på en side som er begravet tre nivåer under hovedsiden flimrer litt når noen klikker det, kan du fikse det, men sannsynligvis vil ikke legge en egen enhet test for denne saken. Økonomien bare rettferdiggjør det ikke.
Testing tankegang er viktig. Et prinsipp jeg bruker er at hvert stykke kode har minst to brukere: den andre koden som bruker den og testen som tester den. Denne enkle regelen hjelper mye med design og avhengigheter. Hvis du husker at du må skrive en test for koden din, vil du ikke legge til mange avhengigheter som er vanskelig å rekonstruere under testing.
For eksempel, anta at koden din må beregne noe. For å kunne gjøre det, må den laste inn noen data fra en database, lese en konfigurasjonsfil, og dynamisk konsultere noen REST API for oppdatert informasjon. Alt dette kan være nødvendig av ulike grunner, men å sette alt det inn i en enkelt funksjon, gjør det ganske vanskelig å prøve enheten. Det er fortsatt mulig med mocking, men det er mye bedre å strukturere koden riktig.
Den enkleste testkoden er rene funksjoner. Rene funksjoner er funksjoner som bare har tilgang til parameterværdiene, har ingen bivirkninger, og returnerer det samme resultatet når de kalles med de samme argumentene. De endrer ikke programmets tilstand, får ikke tilgang til filsystemet eller nettverket. Deres fordeler er for mange til å telle her.
Hvorfor er de enkle å teste? Fordi det ikke er behov for å sette et spesielt miljø for å teste. Du sender bare argumenter og tester resultatet. Du vet også at så lenge koden under test ikke endres, må testen din ikke endres.
Sammenlign det med en funksjon som leser en XML-konfigurasjonsfil. Din test må opprette en XML-fil og sende filnavnet til koden under testen. Ingen stor sak. Men antar at noen bestemte at XML er avskyelig, og alle konfigurasjonsfiler må være i JSON. De går om sin virksomhet og konverterer alle konfigurasjonsfiler til JSON. De driver alle testene, inkludert testene dine og de alle passere!
Hvorfor? Fordi koden ikke endret seg. Det forventer fortsatt en XML-konfigurasjonsfil, og testen din konstruerer fortsatt en XML-fil for den. Men i produksjonen får koden din en JSON-fil, som den ikke klarer å analysere.
Feilbehandling er en annen ting som er kritisk for å teste. Det er også en del av design. Hvem er ansvarlig for korrektheten av input? Hver funksjon og metode bør være tydelig om det. Hvis det er funksjonens ansvar, bør det verifisere innspillingen, men hvis det er oppringerens ansvar, kan funksjonen bare gå om sin virksomhet og anta at inngangen er riktig. Den samlede korrektheten av systemet vil bli sikret ved å ha tester for den som ringer, for å verifisere at den bare sender riktig innspill til funksjonen din.
Vanligvis vil du bekrefte innspillet på det offentlige grensesnittet til koden din fordi du ikke nødvendigvis vet hvem som skal ringe koden din. La oss se på kjøre()
metode for selvkjørende bil. Denne metoden forventer en "destinasjons" -parameter. Parameteren "Destinasjon" vil bli brukt senere i navigasjon, men stasjonsmetoden gjør ingenting for å bekrefte at det er riktig.
La oss anta at destinasjonen skal være et tuple av breddegrad og lengdegrad. Det finnes alle typer tester som kan gjøres for å bekrefte at det er gyldig (for eksempel er målet i midten av havet). For våre formål, la oss bare sørge for at det er en tuple av flyter i området 0,0 til 90,0 for breddegrad og -180,0 til 180,0 for lengdegrad.
Her er oppdatert SelfDrivingCar
klasse. Jeg implementerte trivielt noen av de ikke implementerte metodene fordi kjøre()
Metoden kaller noen av disse metodene direkte eller indirekte.
klasse SelfDrivingCar (objekt): def __init __ (selv, object_detector): self.object_detector = object_detector self.speed = 0 self.destination = Ingen def _accelerate (egen-): self.speed + = 1 def _decelerate (selv): if selv. hastighet> 0: self.speed - = 1 def _advance_to_destination (selv): avstand = self._calculate_distance_to_object_in_front () hvis avstand < 10: self.stop() elif distance < self.speed / 2: self._decelerate() elif self.speed < self._get_speed_limit(): self._accelerate() def _has_arrived(self): return True def _calculate_distance_to_object_in_front(self): return self.object_detector.calculate_distance_to_object_in_front() def _get_speed_limit(self): return 65 def stop(self): self.speed = 0 def drive(self, destination): self.destination = destination while not self._has_arrived(): self._advance_to_destination() self.stop()
For å teste feilhåndtering i testen, vil jeg sende ugyldige argumenter og bekrefte at de er riktig avvist. Du kan gjøre dette ved å bruke self.assertRaises ()
Metode av unittest.TestCase
. Denne metoden lykkes hvis koden under test faktisk gir et unntak.
La oss se det i aksjon. De prøvekjøring()
metode passerer breddegrad og lengdegrad utenfor det gyldige området og forventer at kjøre()
metode for å oppnå et unntak.
fra selvstendig import TestCase fra self_driving_car import SelfDrivingCar klasse MockObjectDetector (objekt): def calculate_distance_to_object_in_front (self): return 20 class SelfDrivingCarTest (TestCase): def setUp (selv): self.car = SelfDrivingCar (MockObjectDetector ()) def test_stop self.car.speed = 5 self.car.stop () # Kontroller at hastigheten er 0 etter avsluttet self.assertEqual (0, self.car.speed) # Kontroller det er OK å stoppe igjen hvis bilen er allerede stoppet selv. car.stop () self.assertEqual (0, self.car.speed) def test_drive (egen-): # Gyldig reisemålet self.car.drive ((55,0, 66,0)) # Invalid destinasjon feil range self.assertRaises (Unntak, selv .car.drive, (-55.0, 200.0))
Testen feiler, fordi kjøre()
Metoden sjekker ikke sine argumenter for gyldighet og gir ikke unntak. Du får en fin rapport med full informasjon om hva som feilet, hvor og hvorfor.
python -m unittest oppdage v test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... FAIL test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok ======================= ============================================================================================================== (untitled.test_self_driving_car.SelfDrivingCarTest) ------------------------------------------- --------------------------- Traceback (siste samtalen sist): File "/Users/gigi/PycharmProjects/untitled/test_self_driving_car.py" , linje 29, i test_drive self.assertRaises (Unntak, self.car.drive, (-55.0, 200.0)) AssertionError: Unntak ikke hevet -------------------- -------------------------------------------------- Ran 2 tester i 0.000s FAILED (feil = 1)
For å fikse det, la vi oppdatere kjøre()
metode for å faktisk sjekke omfanget av sine argumenter:
def drive (selv, destinasjon): lat, lon = destinasjon hvis ikke (0.0 <= lat <= 90.0): raise Exception('Latitude out of range') if not (-180.0 <= lon <= 180.0): raise Exception('Latitude out of range') self.destination = destination while not self._has_arrived(): self._advance_to_destination() self.stop()
Nå går alle testene.
python -m unittest oppdage v test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok ----------------------- ----------------------------------------------- Ran 2 tester i 0.000s OK
Skal du teste alle funksjoner og metoder? Spesielt bør du teste private metoder kalt bare av koden din? Det typiske utilfredsstillende svaret er: "Det avhenger".
Jeg vil prøve å være nyttig her og fortelle deg hva det avhenger av. Du vet nøyaktig hvem som ringer din private metode - det er din egen kode. Hvis testene dine for de offentlige metodene som kaller din private metode er omfattende, tester du allerede dine private metoder uttømmende. Men hvis en privat metode er svært komplisert, kan det være lurt å teste det selvstendig. Bruk din dom.
I et stort system er det ikke alltid klart hvordan man organiserer testene dine. Skal du ha en stor fil med alle tester for en pakke, eller en testfil for hver klasse? Skal testene være i samme fil som koden under test, eller i samme katalog?
Her er systemet jeg bruker. Testene skal være helt skilt fra koden under testen (derfor bruker jeg ikke doktest). Ideelt sett bør koden din være i en pakke. Tester for hver pakke skal i en søskenskatalog av pakken din. I testerkatalogen bør det være en fil for hver modul i pakken du heter test_
.
Hvis du for eksempel har tre moduler i pakken din: module_1.py
, module_2.py
og module_3.py
, du bør ha tre testfiler: test_module_1.py
, test_module_2.py
og test_module_3.py
under testkatalogen.
Denne konvensjonen har flere fordeler. Det gjør det klart bare ved å bla gjennom kataloger som du ikke glemte å teste noen modul helt. Det bidrar også til å organisere testene i rimelig størrelse biter. Forutsatt at modulene dine er rimelig store, vil testkoden for hver modul være i sin egen fil, noe som kan være litt større enn modulen under test, men likevel noe som passer komfortabelt i en fil.
Enhetstester er grunnlaget for solid kode. I denne opplæringen har jeg utforsket noen prinsipper og retningslinjer for enhetstesting og forklart begrunnelsen bak flere beste praksiser. Jo større systemet du bygger, desto viktigere enhetstester blir. Men enhetstester er ikke nok. Andre typer tester er også nødvendige for storskala systemer: integreringstester, ytelsestester, belastningstester, penetrasjonstester, aksepttest, etc..