Advertisement
  1. Code
  2. Coding Fundamentals
  3. Testing

Írjunk professzionális unit teszteket Pythonban

Scroll to top
Read Time: 20 min

() translation by (you can also view the original English article)

Tesztelés az alapja a SOLID szoftver fejlesztésnek. Többféle tesztelési típus létezik, de a legfontosabb típusa a unit tesztelés. Unit tesztekkel eléggé biztos lehetsz abban, hogy jól tesztelt programrészeket használsz mint primitívek, és függhetsz tőlük, amikor összekapcsolod őket, hogy elkészítsd a programodat. A programozási nyelved beépített kódjain és alap könyvtárán túl, ezek a tesztek bővítik a megbízható kódjaid tárházát. Ezen felül a Python nyelv pedig kiváló támogatást nyújt a unit tesztek írásához.

Működő példa

Mielőtt nekilátunk az alapelveknek és az iránymutatásoknak, nézzünk meg egy reprezentatív unit tesztet működés közben. A SelfDrivingCar osztály egy önvezető autó vezetési logikájának részleges implementációja. Főként az autó sebességének szabályozásával foglalkozik. Figyelembe veszi az előtte lévő objektumokat, a sebesség korlátot, és hogy megérkezett-e a cél állomásra, vagy sem.

1
class SelfDrivingCar(object):
2
3
    def __init__(self):
4
5
        self.speed = 0
6
7
        self.destination = None
8
9
        
10
11
    def _accelerate(self):
12
13
        self.speed += 1
14
15
        
16
17
    def _decelerate(self):
18
19
        if self.speed > 0:
20
21
            self.speed -= 1
22
23
                    
24
25
    def _advance_to_destination(self):
26
27
        distance = self._calculate_distance_to_object_in_front()
28
29
        if distance < 10:
30
31
            self.stop()
32
33
34
35
        elif distance < self.speed / 2:
36
37
            self._decelerate()
38
39
        elif self.speed < self._get_speed_limit():
40
41
            self._accelerate()
42
43
    
44
45
    def _has_arrived(self):
46
47
        pass
48
49
        
50
51
    def _calculate_distance_to_object_in_front(self):
52
53
        pass
54
55
        
56
57
    def _get_speed_limit(self):
58
59
        pass
60
61
        
62
63
    def stop(self):
64
65
        self.speed = 0
66
67
        
68
69
    def drive(self, destination):
70
71
        self.destination = destination
72
73
        while not self._has_arrived():            
74
75
            self._advance_to_destination()
76
77
78
        self.stop()
79
80
    def __init__(self):
81
82
        self.speed = 0
83
84
        self.destination = None
85
86
        
87
88
    def _accelerate(self):
89
90
        self.speed += 1
91
92
        
93
94
    def _decelerate(self):
95
96
        if self.speed > 0:
97
98
            self.speed -= 1
99
100
                    
101
102
    def _advance_to_destination(self):
103
104
        distance = self._calculate_distance_to_object_in_front()
105
106
        if distance < 10:
107
108
            self.stop()
109
110
111
112
        elif distance < self.speed / 2:
113
114
            self._decelerate()
115
116
        elif self.speed < self._get_speed_limit():
117
118
            self._accelerate()
119
120
    
121
122
    def _has_arrived(self):
123
124
        pass
125
126
        
127
128
    def _calculate_distance_to_object_in_front(self):
129
130
        pass
131
132
        
133
134
    def _get_speed_limit(self):
135
136
        pass
137
138
        
139
140
    def stop(self):
141
142
        self.speed = 0
143
144
        
145
146
    def drive(self, destination):
147
148
        self.destination = destination
149
150
        while not self._has_arrived():            
151
152
            self._advance_to_destination()
153
154
        self.stop()
155

Itt van egy unit teszt a stop() metódushoz, étvágygerjesztőnek. A részleteket később nézzük meg.

1
from unittest import TestCase
2
3
4
5
class SelfDrivingCarTest(TestCase):
6
7
    def setUp(self):
8
9
        self.car = SelfDrivingCar()
10
11
        
12
13
    def test_stop(self):
14
15
        self.car.speed = 5
16
17
        self.car.stop()
18
19
        # Verify the speed is 0 after stopping

20
21
        self.assertEqual(0, self.car.speed)
22
23
        
24
25
        # Verify it is Ok to stop again if the car is already stopped

26
27
        self.car.stop()
28
29
        self.assertEqual(0, self.car.speed)

Unit tesztelés irányvonalak

Elköteleződés

Jó unit teszteket írni nehéz munka. Unit teszteket írni időbe telik. Amikor módosítasz a kódodon, általában módosítanod kell a tesztjeidet is. Néha bugos lesz a teszt kódod. Ez azt jelenti, hogy nagyon elkötelezettnek kell lenned. Az előnyök hatalmasak, még kis projektek esetén is, de ezek nem ingyenesek.

Légy fegyelmezett

Fegyelmezettnek kell lenned. Légy következetes. Legyél biztos benne, hogy a tesztek mindig zöldek (passed) legyenek. Ne hagyd, hogy a tesztek pirosak (failed) legyenek, mert te úgyis "tudod", hogy a kód OK.

Automatizálás

Ahhoz hogy fegyelmezett legyél, automatizálnod kell a unit tesztjeidet. A teszteknek automatikusan futniuk kell meghatározott események hatására, mint pl. pre-commit vagy pre-deployment. Ideálisan, a forráskód menedzselő rendszerednek vissza kell utasítania azt a kódot, ami nem ment át minden teszten.

Definíció: A teszteletlen kód hibás

Ha nem teszteled a kódod, nem mondhatod, hogy működik. Ez azt jelenti, hogy hibásnak kell tekintened. Ha ez egy kritikus kód, ne deployold éles környezetbe.

Alapok

Mi is az egység (unit)?

A unit tesztelés szempontjából az egység (unit) az egy file/modul, ami bizonyos függvények halmazát vagy egy osztályt tartalmaz. Ha olyan fájlod van, ami több osztályt tartalmaz, akkor mindegyikhez külön unit tesztet kell írnod.

TDD-zni vagy nem TDD-zni

A teszt vezérelt fejlesztés (TDD) egy olyan gyakorlat, amikor a tesztjeidet még a kódolás előtt megírod. Ennek a megközelítésnek jónéhány előnye van, de azt javaslom, hogy most még ne menjünk bele, amíg nem tudunk biztosan helyes teszteket írni.

Ennek az oka az, hogy kódolás közben tervezek. Kódolok, megnézem, újraírom, megnézem újra, és gyorsan újra átírom. Ha először írok tesztet, az korlátoz engem és lelassít.

Amikor kész vagyok a kezdeti tervezéssel, azonnal megírom a teszteket, mielőtt integrálnám a rendszerem többi részéhez. Ez egy nagyszerű módja annak, hogy bevezesd magad a unit tesztek írásába, és így minden kódodhoz lesz teszt is.

A unittest Modul

A Python standard könyvtára tartalmazza a unittest modult. Ez egy TestCase osztályt biztosít, amiből származtathatod a saját osztályodat. Ezután felüldefiniálhatod a setUp() metódust, amivel előkészítjük minden egyes teszt előtt a fix tesztkörnyezetet (fixture) és/vagy a classSetUp() osztály metódust definiáljuk felül, amivel a legelején, az összes teszt előtt egyszer készítünk elő egy fix tesztkörnyezetet (fixture). Utóbbi esetben a különböző tesztek között ekkor nem lesz előkészítve (resetelve) a környezet. A fenti függvények párjai a tearDown() és a classTearDown() metódusok, amiket ugyancsak felüldefiniálhatsz.

Lássuk a releváns részeket a SelfDrivingCarTest osztályunkból. Én most csak a setUp() metódust használom. Készítek egy friss ropogós SelfDrivingCar példányt és eltárolom a self.car-ban, így minden tesztben elérhető lesz.

1
from unittest import TestCase
2
3
4
5
class SelfDrivingCarTest(TestCase):
6
7
    def setUp(self):
8
9
        self.car = SelfDrivingCar()

A következő lépésben megírjuk a specifikus teszt metódusokat, amikkel ellenőrizzük a tesztelés során, hogy a kód - jelen esetben a SelfDrivingCar osztály - azt csinálja-e, amit kell. A teszt metódus struktúrája eléggé általános:

  • Előkészítjük a környezetet (opcionális).
  • Előkészítjük az elvárt értéket.
  • Meghívjuk a kódot a tesztelés során.
  • Ellenőrizzük, hogy a jelenlegi érték megegyezik az elvárt értékkel.

Megjegyzem, hogy a vizsgálandó érték nem szükséges, hogy a metódus kimenete legyen. Lehet egy osztály állapot változása, vagy egy másodlagos folyamat, mint pl. egy új sor beszúrása az adatbázisba, vagy fájlba írás, vagy egy email küldése.

Például, a SelfDrivingCar osztály stop() metódusa nem tér vissza semmivel, de megváltoztatja a belső állapotot azzal, hogy a sebességet 0-ra állítja. Az assertEqual() metódust, amit a TestCase alaposztály biztosít, arra használjuk itt, hogy ellenőrizzük, hogy a stop() hívása úgy működött, ahogy elvártuk.

1
def test_stop(self):
2
3
        self.car.speed = 5
4
5
        self.car.stop()
6
7
        # Verify the speed is 0 after stopping

8
9
        self.assertEqual(0, self.car.speed)
10
11
        
12
13
        # Verify it is Ok to stop again if the car is already stopped

14
15
        self.car.stop()
16
17
        self.assertEqual(0, self.car.speed)

Jelenleg két teszt van most. Az első teszttel megbizonyosodunk arról, ha az autó sebessége 5, és a stop() függvényt meghívtuk, akkor a sebesség 0-ra változik. Aztán, a másik teszttel megnézzük, hogy nem lesz probléma akkor, amikor újra meghívjuk a stop() függvényt, amikor az autó már megállt.

Végül, bemutatok még néhány tesztet, hogy lássunk még funkciókat.

A doctest modul

A doctest module egy érdekes dolog. Segítségével interaktív kód példákat használhatsz a docstringedben (3 idézőjel közötti kód dokumentáció) és ellenőrizheted az eredményeket, a dobott kivételeket (exception) is beleértve.

Én nem használom és nem is javaslom a doctest-et nagyméretű rendszerekben. A helyes unit tesztelés sok munkával jár. A teszt kód tipikusan sokkal nagyobb, mint az a kód, amit tesztelünk. A docstring-ek nem a legmegfelőbb eszközök a részletes tesztekhez. Egyébként menők. Itt van, ahogy a factorial függvény néz ki doc tesztekkel:

1
import math
2
3
4
5
def factorial(n):
6
7
    """Return the factorial of n, an exact integer >= 0.

8


9


10


11
    If the result is small enough to fit in an int, return an int.

12


13
    Else return a long.

14


15


16


17
    >>> [factorial(n) for n in range(6)]

18


19
    [1, 1, 2, 6, 24, 120]

20


21
    >>> [factorial(long(n)) for n in range(6)]

22


23
    [1, 1, 2, 6, 24, 120]

24


25
    >>> factorial(30)

26


27
    265252859812191058636308480000000L

28


29
    >>> factorial(30L)

30


31
    265252859812191058636308480000000L

32


33
    >>> factorial(-1)

34


35
    Traceback (most recent call last):

36


37
        ...

38


39
    ValueError: n must be >= 0

40


41


42


43
    Factorials of floats are OK, but the float must be an exact integer:

44


45
    >>> factorial(30.1)

46


47
    Traceback (most recent call last):

48


49
        ...

50


51
    ValueError: n must be exact integer

52


53
    >>> factorial(30.0)

54


55
    265252859812191058636308480000000L

56


57


58


59
    It must also not be ridiculously large:

60


61
    >>> factorial(1e100)

62


63
    Traceback (most recent call last):

64


65
        ...

66


67
    OverflowError: n too large

68


69
    """
70
71
    if not n >= 0:
72
73
        raise ValueError("n must be >= 0")
74
75
    if math.floor(n) != n:
76
77
        raise ValueError("n must be exact integer")
78
79
    if n+1 == n:  # catch a value like 1e300

80
81
        raise OverflowError("n too large")
82
83
    result = 1
84
85
    factor = 2
86
87
    while factor <= n:
88
89
        result *= factor
90
91
        factor += 1
92
93
    return result
94
95
96
97
98
99
if __name__ == "__main__":
100
101
    import doctest
102
103
    doctest.testmod()

Amint láthatod, a docstring sokkal nagyobb mint a függvény kódja. Nem az olvashatóságot reklámozza.

Tesztek futtatása

OK. Megírtad a unit tesztjeidet. Nagy rendszer esetén, tíz, száz, ezer modulod és osztályod lesz, valószínű több könyvtárban. Hogy fogod futtatni ezeket a teszteket?

A unittest modul különféle eszközöket biztosít a tesztek csoportosítására és programozott futtatására. Nézz utána a Tesztek betöltésének és futtatásának. De a legkönnyebb mód a tesztek feltérképezése (discovery). Ezt az opciót csak Python 2.7-ben vezették be. 2.7 előtt a nose modult használhatod a feltérképezéshez és a tesztek futtatásához. A nose modulnak néhány más előnye is van, mint pl. teszt függvények futtatása, a tesztesetekhez szükséges osztály létrehozása nélkül. De hogy a cikk témájánál maradjunk, térjünk vissza a unittest modulhoz.

Unit teszt alapú tesztjeid feltérképezéséhez és futtatásához csak írd be a parancssorba:

python -m unittest discover

A unittest parancs végignézi az összes file-t és alkönyvtárt, lefuttatja az összes tesztet, amit talál, és egy szép riportot ad vissza a futása során. Ha szeretnéd látni, melyik teszteket futtatja, add hozzá a -v kapcsolót:

python -m unittest discover -v

Van még néhány kapcsoló, amit használhatunk:

1
python -m unittest -h
2
3
Usage: python -m unittest [options] [tests]
4
5
6
7
Options:
8
9
  -h, --help       Show this message
10
11
  -v, --verbose    Verbose output
12
13
  -q, --quiet      Minimal output
14
15
  -f, --failfast   Stop on first failure
16
17
  -c, --catch      Catch control-C and display results
18
19
  -b, --buffer     Buffer stdout and stderr during test runs
20
21
22
23
Examples:
24
25
  python -m unittest test_module               - run tests from test_module
26
27
  python -m unittest module.TestClass          - run tests from module.TestClass
28
29
  python -m unittest module.Class.test_method  - run specified test method
30
31
32
33
[tests] can be a list of any number of test modules, classes and test
34
35
methods.
36
37
38
39
Alternative Usage: python -m unittest discover [options]
40
41
42
43
Options:
44
45
  -v, --verbose    Verbose output
46
47
  -f, --failfast   Stop on first failure
48
49
  -c, --catch      Catch control-C and display results
50
51
  -b, --buffer     Buffer stdout and stderr during test runs
52
53
  -s directory     Directory to start discovery ('.' default)
54
55
  -p pattern       Pattern to match test files ('test*.py' default)
56
57
  -t directory     Top level directory of project (default to
58
59
                   start directory)
60
61
62
63
For test discovery all test modules must be importable from the top
64
65
level directory of the project.

Teszt lefedettség

A teszt lefedettség egy gyakran hanyagolt terület. A lefedettség azt jelenti, hogy a kódodnak mekkora része van valójában tesztelve. Például, ha van egy függvényed egy if-else utasítással, és csak az if ágat teszteled, akkor nem tudod, hogy az else ág működik-e vagy sem. A következő példakódban, az add() függvény ellenőrzi a bementi paramétereinek a típusát. Ha mindkettő egész, akkor csak összeadja őket.

Ha mindkettő sztring, megpróbálja egésszé konvertálni és összeadni. Egyébként kivételt dob. A test_add() függvény teszteli az add() függvényt úgy, hogy mindkét argumentum egész, vagy float, és ellenőrzi a helyes viselkedést mindkét esetben. De a teszt lefedettség nem teljes. A sztring paramétereket nem teszteltük még. A teszt sikeresen lefutott, de az elírást az ágban, ahol mindkét paraméter sztring, nem találta meg (látod az 'intg'-t?).

1
import unittest
2
3
4
5
def add(a, b):
6
7
    """This function adds two numbers a, b and returns their sum

8


9


10


11
    a and b may integers

12


13
    """
14
15
    if isinstance(a, int) and isinstance(b, int):
16
17
        return a + b
18
19
    elseif isinstance(a, str) and isinstance(b, str):
20
21
        return int(a) + intg(b)
22
23
    else:
24
25
        raise Exception('Invalid arguments')
26
27
28
29
class Test(unittest.TestCase):
30
31
    def test_add(self):
32
33
        self.assertEqual(5, add(2, 3))
34
35
        self.assertEqual(15, add(-6, 21))
36
37
        self.assertRaises(Exception, add, 4.0, 5.0)
38
39
40
41
unittest.main()       

Ez a kimenet:

1
----------------------------------------------------------------------
2
3
Ran 1 test in 0.000s
4
5
6
7
OK
8
9
10
11
Process finished with exit code 0

Saját unit tesztek

Ipari szintű unit teszteket írni nem egyszerű és nem könnyű. Van néhány dolog, amit figyelembe kell venni és amiben kompromisszumot kell kötni.

Tesztelhetőség tervezés

Ha a te kódod olyan, amit formálisan spagetti kódnak hívnak, vagy olyan, mint egy nagy sárlabda, ahol a különböző szintű absztrakciók össze vannak keverve, és minden egyes kód részlet függ az összes többi kód részlettől, akkor nagyon nehéz lesz tesztelned. Ráadásul, ha megváltoztatsz valamit, akkor egy csomó tesztedet is frissítened kell.

A jó hír az, hogy a tesztelhetőség érdekében éppen az általános célú pontos szoftver tervezésére van szükséged. Valójában jól strukturált, moduláris kódra van szükség, ahol minden komponensnek egyértelmű felelősségi köre van, és jól definiált interfészeken keresztül van kapcsolatban más komponensekkel. Így játszva lehet jó unit teszteket készíteni.

Például, a SelfDrivingCar osztályunk felelős az autó magas szintű feladataiért: go, stop, navigate. Van egy calculate_distance_to_object_in_front() metódusa, ami még nem lett implementálva. Ezt a funkciót valószínű egy teljesen más rész rendszerként kell implementálni. Tartalmazhat olyan feladatokat, mint pl. különféle szenzorok olvasása, kommunikáció más önjáró autókkal, a teljes látható tartomány feltérképezése, amivel képeket elemez több különböző kamerából.

Lássuk, hogy működik ez a gyakorlatban. A SelfDrivingCar egyik paramétere az object_detector, aminek van egy calculate_distance_to_object_in_front() nevű metódusa, és ez delegálni fogja ezt a funkcionalitást ennek az objektumnak. Most nincs szükség unit tesztelésre, mert az object_detector felelős ezért, amit külön kellene tesztelni. Azt a tényt szeretnénk unit tesztelni, hogy megfelelően használjuk az object_detector-t.

1
class SelfDrivingCar(object):
2
3
    def __init__(self, object_detector):
4
5
        self.object_detector
6
7
        self.speed = 0
8
9
        self.destination = None
10
11
                
12
13
    def _calculate_distance_to_object_in_front(self):
14
15
        return self.object_detector.calculate_distance_to_object_in_front()

Költség / Haszon

A tesztelésbe fektetett erőfeszítésednek korrelációban kell lennie a hiba költségével, mennyire stabil a kód, és hogy mennyire könnyű javítani, ha problémák jönnek elő éles környezetben.

Például az önjáró autó osztályunk biztonság kritikus. Ha a stop() metódus nem működik megfelelően, az önvezető autónk akár embereket ölhet, dolgokat tehet tönkre és alááshatja az önvezető autó piacot is. Ha egy önvezető autót fejlesztesz, gyanítom a stop() metódushoz készült unit tesztjeid sokkal komolyabbak, mint az enyémek.

Másrészről, ha a web alkalmazásodban egy oldalon egy egyszerű gombot veszünk, ami három szintű mélységbe temettél a főoldalon, és arrébb megy egy kicsit, amikor rákattintasz, kijavíthatod, de valószínű nem fogsz egy dedikált unit tesztet készíteni erre az esetre. A gazdaságosság nem engedné.

Hozzáállás tesztelése

A hozzáállás tesztelése is fontos. Egy alapvető dolgot minden kódomnál használok, mégpedig azt, hogy legalább két használója van, egy másik kód, ami használja, és a teszt ami teszteli. Ez az egyszerű szabály sokat segít a tervezésben és a függőségek kialakításában. Ha tudod, hogy tesztet kell írnod a kódodhoz, nem fogsz sok függőséget hozzáadni, amit nehéz a tesztelés során rekonstruálni.

Például, tegyük fel, hogy a kódodnak ki kell számolnia valamit. Ennek érdekében be kell töltenie valamilyen adatot egy adatbázisból, be kell olvasnia egy konfigurációs fájlt, és dinamikusan lekérni valamilyen REST API-tól a friss információkat. Ezekre különböző okok miatt lehet szükség, de mindezt egyetlen függvénybe téve eléggé nehézzé tenné a unit tesztelést. Meg lehetne csinálni mockolással, de sokkal jobb a kódot megfelelően struktruálni.

Tiszta függvények

A legkönnyebb kód, amit tesztelni lehet, azok a tiszta függvények (pure function). A tiszta függvények olyanok, amelyek csak a paramétereik értékeihez férnek hozzá, nincs mellékhatásuk, és ugyanazt a visszatérési értéket adják vissza, amikor ugyanazok a bemeneti paraméterek. Nem változtatják meg a programod állapotát, nem férnek hozzá a fájlrendszerhez vagy a hálózathoz. Annyi előnyük van, hogy nem tudjuk itt összeszedni.

Miért könnyű ezeket tesztelni? Azért, mert nincs szükség a teszteléséhez speciális környezet létrehozására. Csak átadod a paramétert és teszteled az eredményt. És tudhatod azt, hogy amíg a kód nem változik, addig a tesztet sem kell módosítani.

Hasonlítsuk össze egy olyan függvénnyel, ami egy XML konfigurációs fájlt olvas be. A tesztben létre kell hozni egy XML fájlt, és a fájlnevét át kell adni a tesztelendő kódnak. Nem nagy ügy. De aztán valaki úgy döntött, hogy az XML már nem kívánatos, és minden konfigurációs fájlnak JSON-ban kell lennie. Elintézik a munkát, és minden konfigurációs fájlt JSON-ba konvertálnak. Lefuttatják az összes tesztet, beleértve a tieidet is, és mind zöld lesz!

Miért? Mert a kód nem változott. Még mindig XML konfigurációt vár, és a tesztjeid XML fájlt készítenek. De az éles környezetben a kódod JSON fájlt fog kapni, amit nem fog tudni feldolgozni.

Hibakezelés tesztelése

A hibakezelés egy másik kritikus dolog, amit tesztelni kell. Ez is a tervezés része. Ki felelős a bemenet helyességéért? Minden függvény és metódusnak tisztában kell lennie ezzel. Ha a függvény felelőssége, akkor annak ellenőriznie kell a bemeneteit, de ha a hívó felelőssége, akkor a függvény csak elvégzi a dolgát és azt feltételezi, hogy a bemenet helyes. A rendszer teljes helyes működését azok a tesztek biztosítják, amelyek ellenőrzik, hogy a hívó csak helyes bemenetet adnak a függvényednek.

Általában le akarjuk tesztelni a kódunk publikus felületére érkező bemeneteket, mivel szükségszerűen nem tudjuk, ki fogja meghívni a kódunkat. Nézzük meg az önjáró autónk drive() metódusát. Egy 'úticél' paramétert vár. Az 'úticél' paraméter később a navigációnál lesz használva, de a drive metódus nem ellenőrzi egyáltalán.

Feltételezzük, hogy az úticél egy (szélesség, hosszúság) számpárból áll. Sokféle teszt létezik, amivel eldönthető, hogy ez az adat érvényes (pl. az úticél a tenger közepén van-e?) A mi célunknak most megfelel, ha biztosítjuk, hogy ez a float-okból álló számpár, és a szélesség 0.0, 90.0 között van, és a hosszúság -180.0 és 180.0 között.

Itt van a frissített SelfDrivingCar osztályunk. Implementáltam néhány hiányzó metódust, mivel a drive() metódus hívja ezeket közvetve vagy közvetlenül.

1
class SelfDrivingCar(object):
2
3
    def __init__(self, object_detector):
4
5
        self.object_detector = object_detector
6
7
        self.speed = 0
8
9
        self.destination = None
10
11
12
13
    def _accelerate(self):
14
15
        self.speed += 1
16
17
18
19
    def _decelerate(self):
20
21
        if self.speed > 0:
22
23
            self.speed -= 1
24
25
26
27
    def _advance_to_destination(self):
28
29
        distance = self._calculate_distance_to_object_in_front()
30
31
        if distance < 10:
32
33
            self.stop()
34
35
36
37
        elif distance < self.speed / 2:
38
39
            self._decelerate()
40
41
        elif self.speed < self._get_speed_limit():
42
43
            self._accelerate()
44
45
46
47
    def _has_arrived(self):
48
49
        return True
50
51
52
53
    def _calculate_distance_to_object_in_front(self):
54
55
        return self.object_detector.calculate_distance_to_object_in_front()
56
57
58
59
    def _get_speed_limit(self):
60
61
        return 65
62
63
64
65
    def stop(self):
66
67
        self.speed = 0
68
69
70
71
    def drive(self, destination):
72
73
        self.destination = destination
74
75
        while not self._has_arrived():
76
77
            self._advance_to_destination()
78
79
        self.stop()

A hibakezelés teszteléséhez érvénytelen paramétereket adok át, és ellenörzöm, hogy helyesen vissza lesznek utasítva. Ezt a self.assertRaises() metódussal lehet megcsinálni. ami a unittest.TestCase metódusa. Ez a metódus sikeres, ha a tesztelt kód valóban egy kivételt dob.

Lássuk ezt a gyakorlatban. A test_drive() metódus az érvényes tartományon kívüli szélesség és hosszúság értékeket ad át, és azt várja, hogy a drive() metódus kivételt dobjon.

1
from unittest import TestCase
2
3
from self_driving_car import SelfDrivingCar
4
5
6
7
8
9
class MockObjectDetector(object):
10
11
    def calculate_distance_to_object_in_front(self):
12
13
        return 20
14
15
16
17
18
19
class SelfDrivingCarTest(TestCase):
20
21
    def setUp(self):
22
23
        self.car = SelfDrivingCar(MockObjectDetector())
24
25
26
27
    def test_stop(self):
28
29
        self.car.speed = 5
30
31
        self.car.stop()
32
33
        # Verify the speed is 0 after stopping

34
35
        self.assertEqual(0, self.car.speed)
36
37
38
39
        # Verify it is Ok to stop again if the car is already stopped

40
41
        self.car.stop()
42
43
        self.assertEqual(0, self.car.speed)
44
45
46
47
    def test_drive(self):
48
49
        # Valid destination

50
51
        self.car.drive((55.0, 66.0))
52
53
54
55
        # Invalid destination wrong range

56
57
        self.assertRaises(Exception, self.car.drive, (-55.0, 200.0))

A teszt piros lesz, mivel a drive() metódus nem ellenőrzi a paramétereinek az érvényességét, és nem dob kivételt. Egy szép riportot kapunk részletes információval arról, hogy mi lett piros (failed), hol és miért.

1
python -m unittest discover -v
2
3
test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... FAIL
4
5
test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok
6
7
8
9
======================================================================
10
11
FAIL: test_drive (untitled.test_self_driving_car.SelfDrivingCarTest)
12
13
----------------------------------------------------------------------
14
15
Traceback (most recent call last):
16
17
  File "/Users/gigi/PycharmProjects/untitled/test_self_driving_car.py", line 29, in test_drive
18
19
    self.assertRaises(Exception, self.car.drive, (-55.0, 200.0))
20
21
AssertionError: Exception not raised
22
23
24
25
----------------------------------------------------------------------
26
27
Ran 2 tests in 0.000s
28
29
30
31
FAILED (failures=1)

Javítsuk ki, és frissítsük a drive() metódust, hogy ellenőrizze a paramétereinek a tartományát:

1
def drive(self, destination):
2
3
        lat, lon = destination
4
5
        if not (0.0 <= lat <= 90.0):
6
7
            raise Exception('Latitude out of range')
8
9
        if not (-180.0 <= lon <= 180.0):
10
11
            raise Exception('Latitude out of range')
12
13
        
14
15
        self.destination = destination
16
17
        while not self._has_arrived():
18
19
            self._advance_to_destination()
20
21
        self.stop()

Most minden teszt zöld (pass).

1
python -m unittest discover -v
2
3
test_drive (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok
4
5
test_stop (untitled.test_self_driving_car.SelfDrivingCarTest) ... ok
6
7
8
9
----------------------------------------------------------------------
10
11
Ran 2 tests in 0.000s
12
13
14
15
OK
16

Privát metódusok tesztelése

Vajon minden függvényt és metódust tesztelni kell? Pontosabban, privát metódusokat is kell tesztelni, amiket csak a saját kódunk hív meg? A tipikus, nem kielégítő válasz az, hogy "attól függ".

Megpróbálok értelmesen válaszolni erre, és elmondani, hogy mitől függ. Pontosan tudod, hogy ki hívja a privát metódusodat - a saját kódod. Ha kimerítőek és részletesek azok a tesztjeid, amelyek a publikus metódusaidat tesztelik, ami a privát metódusodat hívja, akkor már kimerítően teszteled a privát metódusodat is. De ha a privát metódusod nagyon összetett, akkor lehet hogy külön is tesztelnéd. Neked kell meghozni a döntést.

Hogyan szervezd a unit tesztjeid

Egy nagy rendszerben nem mindig tiszta, hogyan szervezzük a tesztjeinket. Egy nagy fájlba tegyük az összes tesztet a csomaghoz, vagy egy fájl legyen minden osztályhoz? Azonos könyvtárban legyenek a tesztek a kóddal, vagy külön?

Én ezt a rendszert használom. A teszteknek teljesen szeparálva kell lenniük attól a kódtól, amit tesztelnek (ezért nem használok doctest-et). Ideálisan a kódodnak package-ben kell lennie. A teszteknek minden egyes package-hez egy testvér könyvtárban kell lennie a package-dzsel. A teszt könyvtárban, minden modulhoz külön fájlra van szükség, amit így nevezhetünk: test_<module name>.

Például, ha van három modul a package-ben: module_1.pymodule_2.py és module_3.py, akkor három teszt fájlnak kell lennie: test_module_1.py, test_module_2.py és test_module_3.py a teszt könyvtárban.

Ennek a konvenciónak több előnye is van. Csak a könyvtárak böngészéséből egyértelművé válik, ha elfelejtettünk-e egy modult tesztelni. Abban is segít, hogy a tesztjeinket vállalható méretű darabokba rendezzük. Ha feltesszük, hogy a moduljaink megfelelő méretűek, akkor a modulokhoz tartozó tesztkód a saját fájlában lesz, ami talán kicsit nagyobb lesz, mint a modul, amit tesztel, de még mindig akkora, ami kényelmesen elfér egy fájlban.

Összegzés

A SOLID kód alapja a unit tesztelés. Ebben a tutorialban bemutattam néhány alapelvet és irányvonalat a unit teszteléssel kapcsolatban, és elmagyaráztam az okokat a legjobb technikákkal kapcsolatban. Minél nagyobb rendszert építesz, annál fontosabbak lesznek a unit tesztek. De a unit tesztek nem elegendőek. Más féle tesztekre is szükség van a nagyméretű rendszerek esetén, mint pl. integrációs tesztek, performancia tesztek, terhelés tesztek, penetrációs tesztek, elfogadási tesztek.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.