Att skriva enhetstester

By . Latest revision .

Enhetstester, eller unittester, används för att testa att enskilda metoder eller funktioner gör vad vi förväntar oss. Till exempel om en metod ska returnera bool-värdet True, så ska den aldrig kunna returnera False.

Vi ska titta lite närmare på de olika delarna av pythons inbyggda testramverk unittest. Vi hoppar inte i den djupa delen av bassängen, utan vi håller oss vid det grundläggande delarna. Vill du läsa mer kan du kika på docs.python.org.

#Förutsättning

Du kan grunderna i Python och du vet vad variabler, typer och funktioner innebär.

#Varför ska man skriva enhetstester?

Enhetstester skrivs som sagt av anledningen att minimera risken för “trasig” kod och för att validera funktionaliteten. I många lägen handlar det inte enbart om att du ska förstå koden, utan det kan finnas andra utvecklare som tar över ditt projekt eller bara ska hjälpa till. Då är det bra om det är testat ordentligt. Om man har svårt att förstå vad en funktion gör enbart av att läsa koden hjälper det ofta om det finns tester man kan köra och kolla vad olika inputs får för output.

Du vill även skriva tester för din egna skull, att ha bra tester på plats gör att när du skriver om kod eller lägger till ny kan du försäkra dig om att den gamla koden fortfarande gör vad vi förväntar oss. Det är också ett bra sätt att ha koll på buggar, varje gång du hittar en bugg i din kod skapar du ett testfall so kollar att buggen inte introduceras på nytt.

#Vad är ett enhetstest?

Man kan säga att ett enhetstest är en metod som testar en liten del av en applikation för att verifiera delens beteende oberoende från andra delar av applikationen. Ett enhetstest har oftast tre delar:

  • Arrange - Initiera en del av applikation till ett eftersökt tillståndet, t.ex. skapa variabler eller initiera objekt. För enklare tester behöver man inte denna delen.

  • Act - Utför handlingen som vi vill testa, t.ex. anropar en metod.

  • Assert - Sist kontrollerar vi att handlingen vi utförde genomfördes som vi förväntade oss. Oftast genom att göra en “assert” på en funktions returvärde eller kolla värdet på ett objekts attribut.

Om det observerade genomförandet är vad vi förväntade oss passerar testet, annars fallerar det. Om det fallerade indikerar det att något är fel i koden.

#Pythons testramverk

Python kommer med en inbygg modul, ett ramverk kallat “unittest”. Inspirationskällan till det kommer från Javans JUnit. Vi ska framför allt titta på basklassen TestCase som tar hand om enskilda tester på bland annat metoder.

#Kom igång med ett enhetstest

Då så. Vi tittar på grundstrukturen, skapa en fil som heter test.py och lägg till följande kod.

#!/usr/bin/env python3
""" Module for unittests """

import unittest

class TestPhone(unittest.TestCase):
    """ Submodule for unittests, derives from unittest.TestCase """
    pass

if __name__ == '__main__':
    unittest.main()

Vi importerar modulen och skapar en subklass av unittest.TestCase. Blocket med unittest.main() kör igång ett interface för testskriptet och producerar en bra utskrift.

För att skapa ett faktiskt test lägger vi till en metod vars namn börjar med test_. Docstrings som används i metoderna kommer skrivas ut när testfilen körs. Ett enkelt test på den inbyggda funktionen .upper() på strängar kan se ut så här:

#!/usr/bin/env python3
""" Module for unittests """


import unittest

class Testcase(unittest.TestCase):
    """ Submodule for unittests, derives from unittest.TestCase """

    def test_upper(self):
        """ Test builtin uppercase """
        result = 'programmering'.upper()# Act
        self.assertEqual(result, 'PROGRAMMERING')# Assert

if __name__ == '__main__':
    unittest.main()

Vi använder metoden assertEqual för att jämföra om två värden är lika. Följande tabell är hämtad från docs.python.org och visar överskådligt de vanligaste typerna av assert som finns för att säkerställa olika värden. De metoderna finns in basklassen, TestCase.

Method Checks that
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

Om vi nu kör testet får vi utskriften:

python3 test.py
.
----------------------------------------------------
Ran 1 test in 0.000s

OK


>>> python3 test.py -v

test_upper (__main__.Testcase)
Test builtin uppercase ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Med flaggan -v ser vi att vi får en tydligare utskrift, där testerna skrivs ut med. Vi ser att docstringen blir utskriven. Det är trevligt med fina utskrifter så vi kör vidare på det.

Vi kollar också hur det ser ut om testet blir fel.

def test_upper(self):
    """ Test builtin uppercase """
    result = 'programmering'.upper()# Act
    self.assertEqual(result, 'Programmering')# Assert

Istället för att alla bokstäver ska bli stora tänker vi att bara första bokstaven ska bli stor. Med andra ord, vi får ett annat resultat än vad vi förväntar oss.

>>> python3 test.py -v

test_upper (__main__.Testcase)
Test builtin uppercase ... FAIL

======================================================================
FAIL: test_upper (__main__.Testcase)
Test builtin uppercase
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 13, in test_upper
    self.assertEqual(result, 'Programmering')# Assert
AssertionError: 'PROGRAMMERING' != 'Programmering'
- PROGRAMMERING
+ Programmering


----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=1)

Vi kan tydligs se de två olika värdena som jämförs och att de inte är lika med varandra.

#Enhetstesta objekt

Artiklen utgår från filerna som vi hittar i exempelmappen. Där hittar vi klassen Phone i phone.py.

Nu är det dags att titta på hur vi skriver några enhetstester för vår klass, Phone. Klassen ligger i filen phone.py.

Kopiera phone.py och skapa test.py, lägg till #pylint: disable=protected-access i början av filen, då vi kommer använda privata attribut utanför instansen.

En viktig del av att skriva enhetstester är att olika tester inte ska vara beroende av varandra. Med vetskapen att testerna exekverar i bokstavsordning vill vi inte döpa tester till specifika namn för att få dem att exekveras i en viss ordning. T.ex. att i ett test lägga till en kontakt och i nästa test testa en annan metod som har ett beroende på kontakten från förra testet. Då är testerna inte oberoende av varandra, vilket vi vill att de ska vara. Om vi vill att något ska ha skett innan ett test ska vi använda “arrange” fasen för att skapa rätt förutsättningar för testet. I exemplet med Phone klassen behöver vi t.ex. alltid ha ett Phone objekt skapat innan vi kan testa dess metoder. Istället för att skapa ett Phone objekt överst i test filen och låta alla tester använda det objektet ska vi skapa ett nytt Phone objekt till varje testfall.

När det finns gemensamma steg för alla testfall kan vi använda en setUp() metod som körs före varje testmetod exekveras. I den kan vi utföra de steg som alla har gemensamt, i vårt fall skapa ett Phone objekt. Sen kan vi använda metoden tearDwon(), som exekveras efter varje testmetod, för att städa upp efter varje test så det inte finns något kvar från ett tidigare test som kan påverka nästa.

Vi förbereder för att skriva tester i test.py. Importera moduler, skapa test klass och en setUp och tearDown metod.

import unittest
from phone import Phone

class TestPhone(unittest.TestCase):
    """Submodule for unittests, derives from unittest.TestCase"""

    def setUp(self):
        """ Create object for all tests """
        # Arrange
        self.phone = Phone("Samsung", "Galaxy S8", "Android")

    def tearDown(self):
        """ Remove dependencies after test """
        self.phone = None

#Första testet

Då ska vi skriva vårt första test. Vi behöver något att test, något i koden som vi förutsätter ska funka på ett specifikt sätt. T.ex. kan vi verifiera att attributet owner har värdet “No owner yet” i ett ny skapat objekt. I testet vill vi jämföra två strängar, då passar det att använda assertEqual.

    def test_default_owner(self):
        """Test that default value if correct for owner"""
        # Assert
        self.assertEqual(self.phone.owner, "No owner yet")

Om vi vill vara riktigt säkra på att inget är fel i konstruktorn kan vi skapa en test metod som verifierar alla attribut i ett ny skapat objekt. Döp om metoden till test_init och i den verifiera att alla attributen i Phone klassen har förväntat värde i self.phone objektet. Det går att ha flera assert anrop i en metod. Försök själv innan du kollar på koden nedanför.

    def test_init(self):
        """Test that init works as expected"""
        # Assert
        self.assertEqual(self.phone.owner, "No owner yet")
        self.assertEqual(self.phone.manufacturer, "Samsung")
        self.assertEqual(self.phone.model, "Galaxy S8")
        self.assertEqual(self.phone.os, "Android")
        self.assertEqual(self.phone._phonebook, [])

>>> python3 test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.010s

OK

Det var inte så svårt. Vi kollar bara om attributen har rätt värde. När vi går vidare till att testa metoder kan det bli mycket jobbigare.

#Testa metoder

Vi börjar smått och kollar på has_contacts metoden. När vi ska skriva tester för en metod vill vi hitta alla “edge cases” eller olika saker metoden kan resultera i. I detta fallet returnerar metoden True eller False. Då vill vi ha tester som testar båda dessa fallen. Metoden returnerar False när phonebook listan är tom och True när den innehåller minst ett värde. Skriv en test metod som testar att has_contacts returnerar False och använd assert metoden assertFalse för att verifiera värdet.

    def test_empty_phonebook(self):
        """Test that has_contacts return False when phonebook is empty"""
        self.assertFalse(self.phone.has_contacts()) # Assert

Nu vill vi testa metoden när den returnerar True, men då blir det lite jobbigare för att vi blir beroende på andra metoder. För att metoden ska returnera True behöver vi ha ett telefonnummer i listan och de ska egentligen valideras innan de ska läggas till i listan. Så då är has_contacts beroende av två andra metoder, add_contact och validate_number, som behöver köras först. Men då blir testet mer ett test av de andra två metoderna också. Jag väljer att inte använda add_contact för att lägga till en kontakt utan lägger ett nummer direkt i listan. Jag väljer det för att ta bort beroendet på de andra metoderna. Man kan också ignorera det och använda add_contact metoden. Man får avgöra själv hur separerad man vill att testerna ska vara, det finns inget helt rätt eller fel svar. Men oftast vill man bli av med beroenden.

    def test_has_contact_true(self):
        """Test that has_contacts return True when phonebook is has a contact"""
        self.phone._phonebook.append("070-354 78 00") # Arrange
        self.assertTrue(self.phone.has_contacts()) # Assert


>>> python3 test.py  -v
test_empty_phonebook (__main__.TestPhone)
Test that has_contacts return False when phonebook is empty ... ok
test_has_contact_true (__main__.TestPhone)
Test that has_contacts return True when phonebook is has a contact ... ok
test_init (__main__.TestPhone)
Test that init works as expected ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.002s

OK

#Metoder med if-satser och for-loopar

Vi går vidare till en lite större och svårare metod, validate_number. Den innehåller 2,5 if-satser (första if-satsen har ett and vilket jag räknar som en halv) och en for-loop. Här kommer vi in på edge cases, t.ex. om listan som itereras över i for-loopen är tom, verifiera vad som händer då. Eller om ett av villkoren i if-satser med and blir True, vad händer om båda är False och vad händer när båda är True. Det blir snabbt många olika vägar som koden kan ta och där vi vill säkerställa att koden gör som vi förväntar oss.

Vi börjar enkelt och testar med ett nummer som ska validera, “070-354 78 00”. Det är giltigt om det är 13 karaktärer långt och har bindestreck och spaces på tre ställen.

    def test_validate_valid_number(self):
        """Test validating valid number"""
        self.assertTrue(self.phone.validate_number("070-354 78 00"))

Nu till det jobbiga, alla fall som kan ge false. T.ex. om det finns en bokstav, saknar ett space eller bindestreck. Vi börjar med en metod som testar där numret innehåller en bokstav.

    def test_validate_number_with_letter(self):
        """Test validating number with a letter init"""
        self.assertFalse(self.phone.validate_number("070-35b 78 00"))

Vi lägger också in ett test där vi kollar att yttersta if-satsen blir False.

    def test_valid_number_with_missing_space(self):
        """Test validating number with a space missing"""
        self.assertFalse(self.phone.validate_number("070-354 7800"))

>>> python3 test.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.017s

OK

OK, vi nöjer oss med tester för den metoden. Det finns givetvis flera fall som våra tester inte täcker men jag tänker att ni kan komma dem själva.

#Testa exceptions

Hur gör vi med metoder som lyfter exceptions? De behöver vi också verifiera att de lyfts. Självklart finns det en assert metod för det assertRaises(). I get_contact() lyfts ett ValueError om ett namn inte finns i telefonboken. Vi kan börja med ett test som försöker hämta en kontakt om listan är tom.

    def test_get_contact_empty(self):
        """
        Test that error is raised when list is empty
        """
        with self.assertRaises(ValueError) as _:
            self.phone.get_contact("Missing")

Vi använder with för att skapa en context manager som fångar undantaget. Om koden som ligger inom blocket inte lyfter ett exception så kommer testet fallera och säga att inte exception har lyfts. Ni kan testa det genom att kommentera ut get_contact anropet och skriva pass istället och sen köra koden. Då ser det ut som följande.

  File "test.py", line 79, in test_get_contact_fail
    self.phone.get_contact("Zeldah")
AssertionError: ValueError not raised

Vi skapar också ett test som misslyckas när det finns andra kontakter.

    def test_get_contact_fail(self):
        """
        Test that correct value is returned
        when getting contact that does not exist or is empty
        """
        self.phone.add_contact("Andreas", "079-244 07 80")
        with self.assertRaises(ValueError) as _:
            self.phone.get_contact("Zeldah")

Vi lägger till ett test som lyckas också.

    def test_get_contact(self):
        """Test that can get added contact"""
        self.phone.add_contact("Andreas", "079-244 07 80")
        self.assertEqual(self.phone.get_contact("Andreas"),
                         ("Andreas", "079-244 07 80"))

#Testa metoder som anropar andra metoder eller övriga beroenden

Än så länge har vi inte haft några direkta beroenden i metoderna vi testar. När metoder börjar använda installerade moduler eller saker som beror på externa saker som vilket OS programmet körs på eller koppling mot en databas för att få tillbaka ett värde, då blir det genast jobbigare. Då behöver vi bli av med det beroendet när vi kör testerna, så metoderna kan testas i en miljö som inte är hela produktions miljön. Det är poäng med enhetstester, de ska gå snabbt att köra dem och man ska kunna köra dem i sin utvecklings miljö utan databaser och andra dependencies.

Vi går lite överkurs och kollar på mockning/fakes/stubs, ni behöver inte använda detta i kursen. Jag kommer visa några exempel här på Pythons kraftfulla verktyg Mock, om ni vill lära er mer om det kan jag rekommendera artikeln An Introduction to Mocking in Python.

I add_contact är vi beroende av vad metoden validate_number returnerar, den blir vårt yttre beroende som vi inte vill testa när vi skriver tester för add_contact. Vi vill bara testa det som händer i add_contact och den bryr sig bara om ifall validate_number returnerar True eller False.

Vi kan skapa ett Mock objekt som ersätter validate_number metoden. Vi kan då bestämma att metoden ska returnera ett specifikt värde oberoende av vad man skicka in som argument. Det går dessutom att verifiera vilka argument som skickas in till metoden. Vi kollar på ett test där vi lyckas lägga in en användare.

    def test_add_contact_success(self):
        """
        Test we can add contat. Mock validation method.
        """
        # Arrange
        contact = ("Andreas", "079-244 07 80")
        with mock.patch.object(self.phone, 'validate_number') as validate_mock:
            validate_mock.return_value = True

            # Act
            result = self.phone.add_contact(*contact)

            # Assert
            validate_mock.assert_called_once_with(contact[1])
            self.assertTrue(result)
            self.assertEqual(len(self.phone._phonebook), 1)
            self.assertEqual(self.phone._phonebook[0], contact)

Vi börjar med att “patcha” metoden validate_number i objektet self.phone i en context manager. Så för all kod som exekverar i det scopet är metoden ersätt med mock objektet. Efter det bestämmer vi vad som ska returneras när metoden anropas, i detta fallet True.

I act fasen anropar vi vår metod och använder list unpacking för att dela upp tuplen contact så varje element skickas in som separat argument.

När metoden är klar ska vi verifiera att allt skett som vi vill. Vi kollar att den mocka metoden anropas exakt en gång med telefonnumret som argument. Sen kollar vi att det bara finns ett element i phonebook och att det är vår kontakt.

Oftast använder man inte Mock på såna här enkla metoder utan det är mer på externa beroenden, men det visar upp exempel på hur det används. Mock är extremt kraftfullt och enkelt att använda så fort man greppar konceptet.

Testa lägg till ett test där validate_number returnerar False istället.

Om vi kör test filen borde ni minst få denna utskriften:

>>> python3 test.py -v
test_add_contact_success (__main__.TestPhone)
Test we can add contat. Mock validation method. ... ok
test_empty_phonebook (__main__.TestPhone)
Test that has_contacts return False when phonebook is empty ... ok
test_get_contact (__main__.TestPhone)
Test that can get added contact ... ok
test_get_contact_empty (__main__.TestPhone)
Test that error is raised when list is empty ... ok
test_get_contact_fail (__main__.TestPhone)
Test that correct value is returned ... ok
test_has_contact_true (__main__.TestPhone)
Test that has_contacts return True when phonebook is has a contact ... ok
test_init (__main__.TestPhone)
Test that init works as expected ... ok
test_valid_number_with_missing_space (__main__.TestPhone)
Test validating number with a space missing ... ok
test_validate_number_with_letter (__main__.TestPhone)
Test validating number with a letter init ... ok
test_validate_valid_number (__main__.TestPhone)
Test validating valid number ... ok

----------------------------------------------------------------------
Ran 10 tests in 0.017s

OK

#Avslutningsvis

Vi vill bara skriva värdefulla tester. Metoder som bara returnerar ett attribut, där det egentligen inte utförs någon logik och vi kan inte påverka vad som sker i metoden på något sätt finns det inget jätte stort värde i att ha ett test för. Phone innehåller tre metoder av större värde att testa, det är add_contact(), validate_number() och get_contact(). Det är i dem vi har kod som faktiskt utför något.

Många tycker att det är tråkigt och mycket tid. Jag känner ofta likadant när jag sitter med olika projekt, man vill ju bara skriva kod för ny funktionalitet, inte testa koden man redan har skrivit. Men nedanför hittar ni några tankar kring hur man ska prioritera sin teste. Om man verkligen ska testa all kod, då tar det längre tid att skriva testerna än själva koden.

  • Testa vanliga fall i koden. De testerna visar dig om din kod går sönder efter att du har ändrat något.

  • Testa edge cases i några av de med avancerade funktionerna som troligen innehåller fel.

  • När du hittar en bugg, skriv först ett test som kollar det innan du fixar koden så buggen inte finns kvar.

  • Lägg till edge case tester för mindre kritisk kod när du har tid att döda.

Det här var lite om enhetstester och hur man kan gå tillväga för att testa sin kod. De flesta testerna är relativt självförklarande och kommer inte gås in djupare på. Läs gärna mer om enhetstester:

#Revision history

  • 2021-01-29: (C, aar) Skrev om artikeln så den förklarar tänket bakom enhetstester och la till mockning.
  • 2019-01-12: (B, aar) Uppdaterade koden för phone och testerna.
  • 2017-12-12: (A, lew) Updated for v2.

Document source.

Category: oopython.