Introduktion till enhetstester
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 på pythons inbyggda testramverk unittest och hur en katalogstruktur med kod och enhetstester kan se ut. 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. Du har även gjort övningen “Intro till guiden” och “Objekt och klasser” i guiden “Kom igång med objektorienterad programmering i Python”.
#Pythons testramverk
Python kommer med en inbyggd 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.
#Kodstruktur med enhetstester
Använd terminalen och ställ dig i “kmom02”:
$ mkdir unittest $ mkdir unittest/src $ mkdir unittest/tests $ touch unittest/src/__init__.py unittest/src/car.py $ touch unittest/test.py $ touch unittest/tests/__init__.py unittest/tests/test_car.py
Kodstrukturen:
- /src med källkoden, filen med klassen Car
- /tests med koden för enhetstester, alla börjar med test_
- test.py är en testprogrammet som kör alla testfilerna i katalogen /tests tack vare filen “/tests/__init__.py” som gör att alla testfilerna hittas i /tests. Vill du läsa mer om “__init__.py” kan du kika på docs.python.org.
#Klassen som ska testas
Lägg koden för klassen Car i /src/car.py.
#!/usr/bin/env python3
"""
Contains the Car class.
"""
class Car():
""" Car class that handles cars with model and price. """
wheels = 4
def __init__(self, model, price):
""" Constuctor """
self.model = model
self.price = price
def present_car(self):
""" Returns a string presenting the car """
return "This car is of model {m} and costs {p}$.".format(
m=self.model, p=self.price)
#Skapa testprogrammet
Lägg följande kod i test.py som är testprogrammet och kör alla testfiler som ligger i /tests.
#!/usr/bin/env python3
"""
Main program for testing
"""
import unittest
if __name__ == '__main__':
testsuite = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(testsuite)
Flytta dig till unittest och kör följande
$ python3 test.py
#Kom igång med ett enhetstest
Lägg följande kod i /tests/test_car.py. Observera att vägen till filen car.py pekas ut genom “src.car”.
#!/usr/bin/env python3
""" Module for testing the class Car """
import unittest
from src.car import Car
class TestCar(unittest.TestCase):
""" Submodule for unittests, derives from unittest.TestCase """
pass
Kör enhetstesterna igen.
$ python3 test.py
#Kom igång med ett enhetstest
Då ska vi lägga till en metod i testklassen TestCar i /tests/test_car.py som testar att antalet hjul på en instans vi skapar av klassen Car är 4. Byt ut “pass” mot följande kod.
def test_no_of_wheels_ok(self):
""" Test if number of wheels is 4 """
my_car = Car("Volvo", 50000) # Act
self.assertEqual(my_car.wheels, 4)# Assert
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
#Testa en metod
Då ska vi lägga till en ny metod i testklassen TestCar i /tests/test_car.py som testar att metoden “present_car” ger rätt resultat för en Volvo som kostar 50000$. Lägg till följande kod.
def test_present_car_ok(self):
""" Test if the string is correct from present_car """
my_car = Car("Volvo", 50000) # Act
self.assertEqual(my_car.present_car(),
"This bike is of model Volvo and costs 50000$.")# Assert
Aj, då det blev fel! Ändra från “bike” till “car” och testa igen.
#Testa med slumptal
När vi enhetstestar kod med slumptal blir det genast svårare att skriva bra tester. Eftersom det slumpas nya tal varje gång kan vi inte veta i förväg vilket tal testerna ska förvänta sig. Som tur är finns det verktyg som kan hjälpa oss hantera detta. Ett av verktygen är funktionen random.seed() den gör att samma tal alltid slumpas när vi kör testen.
import random
random.seed("car")
print("First number: ", random.randint(1, 100))
print("Second number: ", random.randint(1, 100))
Här blir första slumptalet 11
och andra slumptalet 46
varje gång. Testa gärna!
Vi uppdaterar Car koden till följande för att lägga till att alla bilar får ett unik chassinummer, som slumpas fram när objektet skapas.
import random # nytt
class Car():
""" Car class that handles cars with model and price. """
wheels = 4
def __init__(self, model, price):
""" Constuctor """
self.model = model
self.price = price
self.vin_nr = random.randint(100,999) # nytt
def present_car(self):
""" Returns a string presenting the car """
return "This car is of model {m} and costs {p}$.".format(
m=self.model, p=self.price)
Om ni testar skapa ett objekt och skriva ut vin_nr
kommer det bli ett nytt tal varje gång ni kör programmet. Vi kollar på hur vi hanterar det i testerna.
I testerna lägger vi till metoden setUp()
som automatiskt anropas före alla testfunktioner exekveras. I den anropar vi random.seed()
.
#!/usr/bin/env python3
""" Module for testing the class Car """
import unittest
import random
from src.car import Car
class TestCar(unittest.TestCase):
""" Submodule for unittests, derives from unittest.TestCase """
def setUp(self):
""" Setup that runs before every testcase """
random.seed("car")
...
Nu kan vi lägga till ett testfall som kollar att Car objekt får ett chassinummer.
I testkoden lägger vi till följande metod.
class TestCar(unittest.TestCase):
...
def test_vin_nr_set(self):
""" Test if object get a random vin number """
my_car = Car("Volvo", 50000) # Act
self.assertEqual(my_car.vin_nr, 0)# Assert
Vi kan inte i förväg veta vilket tal som kommer slumpas fram därför sätter vi bara något, jag valde 0
. Nu kör vi testerna en gång och då kommer det bli fel, för talet kommer inte vara 0. Men då får vi fram det talet som alltid kommer slumpas från och med nu, i testerna.
Jag fick utskriften:
====================================================================== FAIL: test_vin_nr_set (__main__.TestCar) Test if object get a random vin number ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/zeldah/git/dbwebb/oopython/me/kmom02/relations/test.py", line 28, in test_vin_nr_set self.assertEqual(my_car.vin_nr, 0)# Assert AssertionError: 186 != 0 ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1)
Här ser vi att med vårt seed kommer första talet som slumpas från och med nu vara 186
. Då uppdaterar vi test koden och ersätter 0
med 186
och testar kör testerna igen. Nu ska alla bli godkända.
---------------------------------------------------------------------- Ran 3 tests in 0.000s OK
Här är hela testklassen.
#!/usr/bin/env python3
""" Module for testing the class Car """
import random
import unittest
from src.car import Car
class TestCar(unittest.TestCase):
""" Submodule for unittests, derives from unittest.TestCase """
def setUp(self): #
""" Setup that runs before every testcase """
random.seed("car")
def test_no_of_wheels_ok(self):
""" Test if number of wheels is 4 """
my_car = Car("Volvo", 50000) # Act
self.assertEqual(my_car.wheels, 4)# Assert
def test_present_car_ok(self):
""" Test if the string is correct from present_car """
my_car = Car("Volvo", 50000) # Act
self.assertEqual(my_car.present_car(),
"This car is of model Volvo and costs 50000$.")# Assert
def test_vin_nr_set(self):
""" Test if object get a random vin number """
my_car = Car("Volvo", 50000) # Act
self.assertEqual(my_car.vin_nr, 186)# Assert
#Testa privata attribut och metoder
Ibland har vi privata attribut eller metoder i en klass som vi ska testa. Då vill vi kunna använda dem utan att bry oss om att de är privata, vilket vi kan för att vi kodar i Python. Men vi kommer fortfarande få valideringsfel om vi gör det.
Ibland kan vi komma runt det genom att använda en get_metod
för att läsa av ett privat attribut istället för att använda attributet direkt, obj._attribut
. Men det passar inte alltid och då är den smidigaste lösning att stänga av valideringen som kollar om man använder privata attribut och metoder. Det gör vi genom att lägga till raden #pylint: disable=protected-access
i början av filen med testerna. Då kommer pylint ignorera när vi skriver obj._attribut
.
#Avslutningsvis
Detta var bara en kort introduktion om enhetstester i pythons unittest. Längre fram i kursen dyker vi ner mer i enhetstester.
Det är viktigt med tester för att upprätthålla kvalitet i koden. Det finns företag som kör med testdriven utveckling. Då utvecklar man först enhetstesterna utifrån kraven som finns på koden. Därefter utvecklas den vanliga koden och du kan testa din kod under utveckling.
#Revision history
- 2024-01-09: (C, aar) Uppdaterat random exemplet så talet slumpas fram i klassen.
- 2023-01-10: (B, grm) Uppdaterat till kmom02.
- 2022-01-12: (A, grm) Första versionen, utgått från “unittest_i_python”.