Skriv professionella enhetsprov i Python

Testning är grunden för solid mjukvaruutveckling. Det finns många typer av test, men den viktigaste typen är enhetstestning. Enhetstestning ger dig mycket förtroende för att du kan använda välprövade bitar som primitiva och lita på dem när du komponerar dem för att skapa ditt program. De ökar din inventering av betrodd kod utöver ditt språkinbyggda och standardbibliotek. Dessutom ger Python bra stöd för att skriva enhetstester.

Löpande exempel

Innan du dyker in i alla principer, heuristik och riktlinjer, låt oss se ett representativt test i handling. De SelfDrivingCar klassen är ett partiellt genomförande av körlogiken hos en självkörande bil. Det handlar mest om att kontrollera bilens hastighet. Den är medveten om föremål framför den, hastighetsgränsen och huruvida den kom fram till dess destination. 

klass SelfDrivingCar (objekt): def __init __ (self): self.speed = 0 self.destination = Ingen def _accelerate (self): self.speed + = 1 def _decelerate (self): om self.speed> 0: self.speed - = 1 def _advance_to_destination (self): distance = self._calculate_distance_to_object_in_front () om avstånd < 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 (self): distance = self._calculate_distance_to_object_in_front () om avstånd < 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() 

Här är ett enhetstest för sluta() metod för att göra din aptit. Jag kommer in i detaljerna senare. 

från självständig import TestCase-klass SelfDrivingCarTest (TestCase): def setUp (self): self.car = SelfDrivingCar () def test_stop (self): self.car.speed = 5 self.car.stop () # Kontrollera hastigheten är 0 efter stoppa self.assertEqual (0, self.car.speed) # Kontrollera att det är Ok att sluta igen om bilen redan är stoppad self.car.stop () self.assertEqual (0, self.car.speed)

Enhetsprovningsriktlinjer

Begå

Att skriva bra enhetstest är hårt arbete. Skrivningsenhetstest tar tid. När du ändrar din kod måste du vanligtvis också ändra dina test. Ibland har du fel i din testkod. Det betyder att du måste vara verkligen engagerad. Fördelarna är enorma, även för små projekt, men de är inte lediga.

Be Disciplined

Du måste vara disciplinerad. Var konsekvent. Kontrollera att testen alltid passerar. Låt inte testen brytas eftersom du vet att koden är OK.

Automatisera

För att hjälpa dig att vara disciplinerad borde du automatisera din enhetstest. Testerna ska köras automatiskt vid viktiga punkter som förhandsbegränsning eller förutplacering. Helst avvisar ditt källkontrollsystem ett kod som inte passerade alla sina test.

Otestad kod är bruten enligt definition

Om du inte testa det kan du inte säga att det fungerar. Det betyder att du bör överväga att den är trasig. Om det är kritisk kod, använd inte det till produktion.

Bakgrund

Vad är en enhet?

En enhet för enhetstestning är en fil / modul som innehåller en uppsättning relaterade funktioner eller en klass. Om du har en fil med flera klasser ska du skriva ett prov för varje klass.

Till TDD eller inte till TDD

Testdriven utveckling är en övning där du skriver testerna innan du skriver koden. Det finns flera fördelar med detta tillvägagångssätt, men jag rekommenderar att du undviker det om du har disciplinen att skriva rätt test senare. 

Anledningen är att jag designar med kod. Jag skriver kod, tittar på den, skriver om den, tittar på den igen och skriver om den igen mycket snabbt. Skrivprov begränsar mig först och saktar mig ner. 

När jag är klar med den ursprungliga designen skriver jag testen omedelbart innan jag integrerar med resten av systemet. Med detta sagt är det ett bra sätt att presentera dig självtesttest och det säkerställer att hela din kod kommer att ha test.

Den Unittest Modulen

Den unittest modulen levereras med Pythons standardbibliotek. Det ger en klass kallad Testfall, som du kan härleda din klass från. Då kan du åsidosätta a inrätta() Metod för att förbereda en testfixtur före varje test och / eller a classSetUp () klassmetod för att förbereda en testfixtur för alla tester (ej återställd mellan individuella test). Det finns motsvarande riva ner() och classTearDown () metoder du kan åsidosätta också.

Här är relevanta delar från vår SelfDrivingCarTest klass. Jag använder bara inrätta() metod. Jag skapar en ny SelfDrivingCar instans och lagra den i self.car så det är tillgängligt för varje test.

från unittest import TestCase klass SelfDrivingCarTest (TestCase): def setUp (self): self.car = SelfDrivingCar ()

Nästa steg är att skriva specifika testmetoder för att testa den kod som testas SelfDrivingCar klass i det här fallet - gör vad den ska göra. Strukturen för en testmetod är ganska standard:

  • Förbered miljön (valfritt).
  • Förbered det förväntade resultatet.
  • Ring koden under testet.
  • Anger att det faktiska resultatet matchar det förväntade resultatet.

Observera att resultatet inte behöver vara resultatet av en metod. Det kan vara en klassbytesändring, en bieffekt som att lägga till en ny rad i en databas, skriva en fil eller skicka ett mail.

Till exempel, sluta() metod för SelfDrivingCar klassen returnerar ingenting, men det ändrar det interna tillståndet genom att ställa hastigheten till 0. Den assertEqual () metod som tillhandahålls av Testfall basklass används här för att verifiera samtalet sluta() arbetade som förväntat.

def test_stop (self): self.car.speed = 5 self.car.stop () # Verifiera hastigheten är 0 efter att ha stoppat self.assertEqual (0, self.car.speed) # Verifiera att det är Ok att sluta igen om bilen är redan stoppad self.car.stop () self.assertEqual (0, self.car.speed)

Det finns faktiskt två tester här. Det första testet är att se till att om bilens hastighet är 5 och sluta() kallas då blir hastigheten 0. Då är ett annat test för att säkerställa att inget går fel om man ringer sluta() igen när bilen redan är stoppad.

Senare introducerar jag flera fler test för ytterligare funktionalitet.

Doktormodulen

Doktormodulen är ganska intressant. Det låter dig använda interaktiva kodprover i ditt dokument och verifiera resultaten, inklusive upphöjda undantag. 

Jag använder eller rekommenderar inte doktest för storskaliga system. Korrekt testning av enheten kräver mycket arbete. Testkoden är typiskt mycket större än koden under testet. Docstrings är bara inte det rätta mediet för att skriva omfattande test. De är dock coola. Här är vad a faktoriell funktion med doc-tester ser ut som:

Importera mattefaktoriella (n): "" "Återför faktorial av n, ett exakt heltal> = 0. Om resultatet är tillräckligt litet för att passa in ett int, returnera en int. Else returnera en lång. >>> [factorial (n) för n inom intervallet (6)] [1, 1, 2, 6, 24, 120] >>> [faktoriell (lång (n)) för n inom intervallet (6)] [1, 1, 2, 6, 24, 120] >>> factorial (30) 265252859812191058636308480000000L >>> factorial (30L) 265252859812191058636308480000000L >>> factorial (-1) Traceback (senaste samtal senast): ... ValueError: n måste vara> = 0 Faktorer av flottörer är ok, men flottören måste vara ett exakt heltal: >>> factorial (30.1) Traceback (senaste samtal senast): ... ValueError: n måste vara exakt heltals >>> factorial (30.0) 265252859812191058636308480000000L Det får inte heller vara löjligt stort : >>> factorial (1e100) Traceback (senaste samtal senast): ... OverflowError: n för stor "" "om inte n> = 0: höja ValueError (" n måste vara> = 0 ") om math.floor )! = n: höja ValueError ("n måste vara exakt heltal") om n + 1 == n: # fånga ett värde som 1e300 höja ÖverflowE rror ("n för stor") resultat = 1 faktor = 2 medan faktor <= n: result *= factor factor += 1 return result if __name__ == "__main__": import doctest doctest.testmod()

Som du kan se är docstringen mycket större än funktionskoden. Det främjar inte läsbarheten.

Runningtest

OK. Du skrev din enhetstester. För ett stort system har du tiotals / hundratals / tusentals moduler och klasser över eventuellt flera kataloger. Hur kör du alla dessa test?

Den unittest modulen ger olika möjligheter att gruppera test och köra dem programmässigt. Kolla in laddnings- och körningstest. Men det enklaste sättet är provfunn. Detta alternativ har bara lagts till i Python 2.7. Pre-2.7 du kan använda näsan för att upptäcka och köra test. Näsan har några andra fördelar som körningsfunktioner utan att behöva skapa en klass för dina testfall. Men för syftet med den här artikeln, låt oss hålla fast vid unittest.

För att upptäcka och köra dina unittest-baserade tester, skriv bara på kommandoraden:

python -m unittest upptäcka

unittest skannar alla filer och underkataloger, kör alla tester som den finner och ger en bra rapport samt körtid. Om du vill se vilka tester som körs kan du lägga till -v-flaggan:

python -m unittest upptäcka -v

Det finns flera flaggor som styr operationen:

python -m unittest -h Användning: python -m unittest [alternativ] [tester] Alternativ: -h, --help Visa detta meddelande -v, --verbose Verbose output -q, --quiet Minimal output -f, - failfast Stoppa vid första misslyckandet -c, --catch Catch control-C och visa resultat -b, --buffert Buffer stdout och stderr under testkörningar Exempel: python -m unittest test_module - kör test från test_module python -m unittest module.TestClass - kör test från modul.TestClass python -m unittest module.Class.test_method - kör specificerad testmetod [tester] kan vara en lista över ett antal testmoduler, klasser och testmetoder. Alternativ användning: python -m unittest upptäcka [alternativ] Alternativ: -v, --verbose Verbose output -f, --failfast Stopp vid första felet -c, --catch Catch control-C och visa resultat -b, -buffert Buffert stdout och stderr under test körningar -s katalog katalog för att starta upptäckt ('.' Default) -p mönster Mönster för att matcha testfiler ('test * .py' standard) -t-katalog Toppnivåkatalog för projekt (standard för att starta katalogen ) För providentifiering måste alla testmoduler importeras från projektets översta nivåkatalog.

Testtäckning

Testtäckning är ett ofta försummat fält. Täckning betyder hur mycket din kod faktiskt testas av dina test. Om du till exempel har en funktion med en om annat uttalande och du testar endast om gren, då vet du inte om annan grenar eller inte. I följande kodexempel, funktionen Lägg till() kontrollerar typen av dess argument. Om båda är heltal, lägger det bara till dem. 

Om båda är strängar försöker den konvertera dem till heltal och lägger till dem. Annars uppstår ett undantag. De test_add () funktionstester Lägg till() Funktion med argument som är både heltal och med argument som flyter och verifierar det korrekta beteendet i varje enskilt fall. Men testdäckningen är ofullständig. Fallet med strängargument kunde inte testas. Som ett resultat passerar testet framgångsrikt, men typsnittet i grenen där argumenten är båda strängarna upptäcktes inte (se "intg" där?).

importera unittest def add (a, b): "" "Denna funktion lägger till två tal a, b och returnerar summan a och b kan heltal" "" om isinstans (a, int) och isinstance (b, int) + b elseif isinstance (a, str) och isinstance (b, str): return int (a) + Intg (b) annat: höja Exception ( 'Ogiltiga argument') klass Test (unittest.TestCase): def test_add (egen) : self.assertEqual (5, add (2, 3)) self.assertEqual (15, add (-6, 21)) self.assertRaises (Undantag, lägg till, 4,0, 5,0) unittest.main () 

Här är utgången:

---------------------------------------------------------------------- Ran 1 test i 0.000s OK Process färdig med utgångskod 0

Hands-On Unit Tests

Skrivning av test av industriell styrka är inte lätt eller enkelt. Det finns flera saker att tänka på och avvägningar ska göras.

Design för testbarhet

Om din kod är det som kallas formellt spaghettikod eller en stor boll av lera där olika nivåer av abstraktion blandas ihop och varje bit av kod beror på varje annat kodstycke, har du svårt att testa den. Också, när du ändrar något, måste du också uppdatera en massa test.

Den goda nyheten är att generell korrekt programdesign är exakt vad du behöver för testbarhet. I synnerhet välmodifierad modulär kod, där varje komponent har tydligt ansvar och samverkar med andra komponenter via väldefinierade gränssnitt, kommer att göra att skriva goda enheter testar ett nöje.

Till exempel vår SelfDrivingCar klassen är ansvarig för hög nivå drift av bilen: gå, stoppa, navigera. Den har en calculate_distance_to_object_in_front () metod som inte har implementerats än. Denna funktionalitet bör antagligen genomföras av ett helt separat delsystem. Det kan innehålla läsdata från olika sensorer, interagera med andra självkörande bilar, en hel maskinvisningsstack för att analysera bilder från flera kameror.

Låt oss se hur detta fungerar i praktiken. De SelfDrivingCar kommer att acceptera ett argument som heter object_detector som har en metod som heter calculate_distance_to_object_in_front (), och det kommer att delegera denna funktionalitet till det här objektet. Nu behöver man inte prova detta på grund av att object_detector är ansvarig (och bör testas) för den. Du vill fortfarande pröva det faktum att du använder object_detector ordentligt.

klass SelfDrivingCar (objekt): def __init __ (själv, object_detector): self.object_detector self.speed = 0 self.destination = Ingen def _calculate_distance_to_object_in_front (egen): retur self.object_detector.calculate_distance_to_object_in_front ()

Kostnad / Fördel

Mängden ansträngning du lägger i testning bör korreleras med felkostnaden, hur stabil koden är och hur lätt det är att åtgärda om problem upptäcks längs linjen.

Till exempel är vår självkörande bilklass superkritisk. Om sluta() Metoden fungerar inte ordentligt, vår självkörande bil kan döda människor, förstöra egendom och spåra hela den självkörande bilmarknaden. Om du utvecklar en självkörande bil, misstänker jag din enhetstester för sluta() Metoden blir lite mer stringent än min. 

Å andra sidan, om en enda knapp i din webbapplikation på en sida som är begravd, tre nivåer under din startsida blinkar lite när någon klickar på det, kan du fixa det, men förmodligen kommer inte att lägga till ett dedikerat test för enheten för det här fallet. Ekonomin motiverar det inte. 

Testa tanken

Testa tankegång är viktigt. En princip som jag använder är att varje kod har minst två användare: den andra koden som använder den och det test som testar det. Denna enkla regel hjälper mycket med design och beroenden. Om du kommer ihåg att du måste skriva ett test för din kod, lägger du inte till många beroenden som är svåra att rekonstruera under testning.

Antag exempelvis att din kod behöver beräkna något. För att kunna göra det måste den ladda vissa data från en databas, läsa en konfigurationsfil och dynamiskt samråda med några REST API för aktuell information. Allt detta kan krävas av olika anledningar, men om allting till en enda funktion blir det ganska svårt att prova enhet. Det är fortfarande möjligt med mocking, men det är mycket bättre att strukturera din kod korrekt.

Rena funktioner

Den enklaste koden att testa är rena funktioner. Rena funktioner är funktioner som endast har tillgång till parametervärdena, har inga biverkningar och ger samma resultat när de kallas med samma argument. De ändrar inte ditt programs tillstånd, har inte tillgång till filsystemet eller nätverket. Deras fördelar är för många att räkna här. 

Varför är de lätta att testa? Eftersom det inte finns något behov av att ställa in en speciell miljö för att testa. Du skickar bara argument och testar resultatet. Du vet också att så länge koden under testet inte förändras behöver ditt test inte ändras. 

Jämför det med en funktion som läser en XML-konfigurationsfil. Ditt test måste skapa en XML-fil och skicka sitt filnamn till koden som testas. Ingen stor grej. Men anta att någon bestämde att XML är avskyvärt och alla konfigurationsfiler måste vara i JSON. De går om sin verksamhet och konverterar alla konfigurationsfiler till JSON. De kör alla tester inklusive dina test och de Allt passera! 

Varför? Eftersom koden inte ändrades. Det förväntar sig fortfarande en XML-konfigurationsfil, och ditt test konstruerar fortfarande en XML-fil för den. Men i produktion kommer din kod att få en JSON-fil, som det inte kommer att analysera.

Testning av felhantering

Felhantering är en annan sak som är kritisk för att testa. Det är också en del av designen. Vem ansvarar för riktigheten av inmatningen? Varje funktion och metod ska vara tydlig om det. Om det är funktionens ansvar, ska det verifiera inmatningen, men om det är den som ringer är ansvaret, kan funktionen bara handla om sin verksamhet och antar att ingången är korrekt. Systemets övergripande korrekthet säkerställs genom att testerna för den som ringer har verifierat att den bara skickar korrekt inmatning till din funktion.

Vanligtvis vill du verifiera inmatningen i det offentliga gränssnittet till din kod eftersom du inte nödvändigtvis vet vem som ska ringa din kod. Låt oss titta på kör() Metod för den självkörande bilen. Denna metod förväntar sig en "destination" -parameter. Parametern "Destination" kommer senare att användas i navigeringen, men det går inte att kontrollera att det är korrekt med drivmetoden. 

Låt oss anta att destinationen är tänkt att vara ett tupel av latitud och longitud. Det finns alla typer av test som kan göras för att verifiera att det är giltigt (t ex är målet mitt i havet). För våra ändamål, låt oss bara se till att det är en tuppel av flottörer i intervallet 0,0 till 90,0 för latitud och -180,0 till 180,0 för longitud.

Här är den uppdaterade SelfDrivingCar klass. Jag implementerade trivialt några av de otillförda metoderna eftersom kör() metod kallar några av dessa metoder direkt eller indirekt.

klass SelfDrivingCar (objekt): def __init __ (själv, object_detector): self.object_detector = object_detector self.speed = 0 self.destination = Ingen def _accelerate (egen): self.speed + = 1 def _decelerate (egen): om själv. hastighet> 0: self.speed - = 1 def _advance_to_destination (self): distance = self._calculate_distance_to_object_in_front () om avstånd < 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()

För att testa felhantering i testet kommer jag att skicka ogiltiga argument och verifiera att de är korrekt avvisade. Du kan göra detta genom att använda self.assertRaises () metod av unittest.TestCase. Denna metod lyckas om koden under test faktiskt ger upphov till ett undantag.

Låt oss se det i aktion. De provköra() Metoden passerar latitud och longitud utanför det giltiga intervallet och förväntar sig kör() metod för att göra ett undantag.

från självständig import TestCase från self_driving_car importera SelfDrivingCar klass MockObjectDetector (object): def calculate_distance_to_object_in_front (själv): returnera 20 klass SelfDrivingCarTest (TestCase): def setUp (self): self.car = SelfDrivingCar (MockObjectDetector ()) def test_stop self.car.speed = 5 self.car.stop () # Verifiera hastigheten är 0 efter att ha stoppat self.assertEqual (0, self.car.speed) # Verifiera att det är Ok att sluta igen om bilen redan är stoppad själv. car.stop () self.assertEqual (0, self.car.speed) def test_drive (egen): # Gäller destination self.car.drive ((55,0, 66,0)) # Ogiltig destination fel range self.assertRaises (undantag, själv .car.drive, (-55,0, 200,0))

Testet misslyckas, eftersom kör() Metoden kontrollerar inte dess argument för giltighet och ger inget undantag. Du får en bra rapport med fullständig information om vad som misslyckades, var och varför.

python -m unittest upptäcka -v test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... FAIL test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok ======================= ================================================= FAIL: test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ------------------------------------------- --------------------------- Traceback (senaste samtal senast): File "/Users/gigi/PycharmProjects/untitled/test_self_driving_car.py" , linje 29, i test_drive self.assertRaises (Undantag, self.car.drive, (-55.0, 200.0)) AssertionError: Undantag ej upptagen -------------------- -------------------------------------------------- Ran 2 test i 0.000s FAILED (fel = 1)

För att fixa det, låt oss uppdatera kör() metod för att faktiskt kontrollera omfattningen av sina argument:

def drive (själv, destination): lat, lon = destination om inte (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()

Nu passerar alla prov.

python -m unittest upptäcka -v test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok ----------------------- ----------------------------------------------- Ran 2 test i 0.000s OK 

Testa privata metoder

Ska du testa varje funktion och metod? Särskilt bör du testa privata metoder som bara kallas med din kod? Det typiskt otillfredsställande svaret är: "Det beror på". 

Jag ska försöka vara användbar här och berätta vad det beror på. Du vet exakt vem som kallar din privata metod - det är din egen kod. Om dina test för de offentliga metoderna som kallar din privata metod är omfattande, testar du redan dina privata metoder uttömmande. Men om en privat metod är väldigt komplicerad kanske du vill testa det självständigt. Använd din dom.

Hur man organiserar enhetstesterna

I ett stort system är det inte alltid klart hur man organiserar dina test. Ska du ha en stor fil med alla tester för ett paket, eller en testfil för varje klass? Ska testen vara i samma fil som koden som testas eller i samma katalog?

Här är systemet jag använder. Testerna ska vara helt skilda från koden som testas (därför använder jag inte doktest). Helst bör din kod vara i ett paket. Testerna för varje förpackning borde finnas i en syskonskatalog över ditt paket. I testkatalogen ska det finnas en fil för varje modul i ditt paket som heter testa_

Om du till exempel har tre moduler i ditt paket: module_1.py, module_2.py och module_3.py, du borde ha tre testfiler: test_module_1.py, test_module_2.py och test_module_3.py under testkatalogen. 

Denna konvention har flera fördelar. Det klargörs bara genom att bläddra i kataloger som du inte glömde att testa någon modul helt. Det bidrar också till att organisera testerna i rimliga storlekar. Om du antar att dina moduler är rimliga, kommer testkoden för varje modul att vara i sin egen fil, vilket kan vara lite större än modulen som testas, men fortfarande något som passar bekvämt i en fil. 

Slutsats

Enhetstest är grunden för fast kod. I denna handledning undersökte jag några principer och riktlinjer för enhetstestning och förklarade resonemanget bakom flera bästa metoder. Ju större det system du bygger, desto viktigare enhetstester blir. Men enhetstest är inte tillräckligt. Andra typer av test behövs också för storskaliga system: integrationsprov, prestandatest, belastningstest, penetrationsprov, godkännande tester, etc.