Advertisement
  1. Code
  2. Python

Írjunk professzionális unit teszteket Pythonban

by
Difficulty:IntermediateLength:LongLanguages:

Hungarian (Magyar) translation by Peter Matyus-Jarai (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.

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

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.

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.

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:

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:

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?).

Ez a kimenet:

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.

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.

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.

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.

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

Most minden teszt zöld (pass).

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
Advertisement
Advertisement
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.