2. diel - Prvý unit test v unittest frameworku
V minulej lekcii, Úvod do testovania softvéru v Pythone , sme si urobili pomerne solídny úvod do problematiky. Tiež sme si uviedli V-model, ktorý znázorňuje vzťah medzi jednotlivými výstupmi fáz návrhu a príslušnými testami.
Testy teda píšeme vždy na základe návrhu, nie implementácie. Inými slovami, robíme ich na základe očakávanej funkčnosti. Tá môže byť buď priamo od zákazníka (a to v prípade akceptačných testov) alebo už od programátora (architekta), kde špecifikuje ako sa má ktorá metóda správať. Dnes sa budeme venovať práve týmto testom, ktorým hovoríme jednotkové (unit testy) a ktoré testujú detailnú špecifikáciu aplikácie, teda jej triedy.
Pamätajte, že nikdy nepíšeme testy podľa toho, ako je niečo vo vnútri naprogramované! Veľmi jednoducho by to mohlo naše myslenie zviesť len tým daným spôsobom a zabudli by sme na to, že metóde môžu prísť napríklad aj iné vstupy, na ktoré nie je vôbec pripravená. Testovanie s implementáciou v skutočnosti vôbec nesúvisí, vždy testujeme, či je splnené zadanie.
Aké triedy testujeme
Unit testy testujú jednotlivé metódy v triedach. Pre istotu zopakujem, že nemá veľký zmysel testovať jednoúčelové metódy, ktoré napr. iba niečo vyberajú z databázy. Aby sme boli konkrétnejší, nemá zmysel testovať metódu ako je táto:import sqlite3 as db def vloz_polozku(nazev, cena): conn = db.connect('aplikace.db') cur = conn.cursor() try: cur.execute('INSERT INTO aplikace (nazev, cena) VALUES(?, ?)', (nazev, cena)) except db.OperationalError: print('Chyba při komunikaci s databází') cur.close() conn.commit() conn.close()
Metóda pridáva položku do databázy. Typicky je použitá len v nejakom formulári a pokiaľ by nefungovala, zistí to akceptačné testy, pretože by sa nová položka neobjavila v zozname. Podobných metód je v aplikácii veľa a zbytočne by sme strácali čas pokrývaním niečoho, čo ľahko pokryjeme v iných testoch.
Unit testy nájdeme najčastejšie pri knižniciach, teda
nástrojoch, ktoré programátor používa na viacerých
miestach alebo dokonca vo viacerých projektoch a mali by byť 100%
funkčné. Možno si spomeniete, kedy ste použili nejakú knižnicu, stiahnutú
napr. z GitHubu. Veľmi pravdepodobne u nej boli aj testy, ktoré sa
najčastejšie vkladajú do zložky test/
, ktorá je vedľa zložky
src/
v adresárovej štruktúre projektu. Pokiaľ napr. píšeme
aplikáciu, v ktorej často potrebujeme nejaké matematické výpočty, napr.
faktoriály a ďalšie pravdepodobnostné funkcie, je samozrejmosťou vytvoriť
si na tieto výpočty knižnicu a je veľmi dobrý nápad pokryť takúto
knižnicu testami.
Príklad
Ako asi tušíte, my si podobnú triedu vytvoríme a skúsime si ju otestovať. Aby sme sa nezdržiavali, vytvorme si iba jednoduchú kalkulačku, ktorá bude vedieť:- sčítať
- odčítať
- násobiť
- deliť
Vytvorenie projektu
V praxi by v triede boli nejaké zložitejšie výpočty, ale tým sa tu zaoberať nebudeme. Vytvorme si nový súbor s názvomkalkulacka.py
a do neho si pridajme triedu Kalkulacka
a nasledujúcu implementáciu:
class Kalkulacka(): # reprezentuje jednoduchou kalkulačku # proměnná a v níže uvedených funkcích je první číslo, # proměnná b číslo druhé def secti(a, b): # sečte 2 čísla a vrátí jejich součet return a + b def odecti(a, b): # odečte 2 čísla a vrátí jejich rozdíl return a - b def vynasob(a, b): # vynásobí 2 čísla a vrátí jejich součin return a * b def vydel(a, b): # vydělí 2 čísla a vrátí jejich podíl # v případě dělení nulou vyvolá chybu if b == 0: raise ValueError('Nelze dělit nulou!') return a / b
Na kóde je zaujímavá iba metóda vydel()
, ktorá vyvolá
výnimku v prípade, že delíme nulou.
Generovanie testov
V Pythone pre testovanie jednotiek (unit testing) používame framework s príhodným názvom unittest, ktorý bol pôvodne inšpirovaný JUnit z jazyka Java a je už súčasťou základného balíčka knižníc, nemusíme teda nič inštalovať.Testy väčšinou nepíšeme do rovnakého súboru, ale pre prehľadnosť
vložíme kód do nového súboru, ktorý podľa konvencie pomenujeme
test_ názov, napr. test_kalkulacka.py
.
V prvom rade si naimportujeme modul unittest
a triedu
Kalkulacka
zo súboru, ktorý budeme testovať
(kalkulacka.py
). Potom si vytvoríme testovaciu triedu, ktorá je
odvodená od triedy TestCase
testovacieho modulu
unittest
a zdedí z nej množstvo užitočných metód, ktoré
neskôr využijeme. Asi vás v objektovom Pythone neprekvapí, že je test
triedy (scenár) reprezentovaný aj triedou a jednotlivé testy metódami.
Do triedy je zvykom pridať classmethod s názvom
setUpClass(cls)
, ktorá sa zavolá raz na začiatku, pred
všetkými testami a tearDownClass(cls)
, ktorá funguje podobne,
zavolá sa raz na konci, až všetky testy prebehnú. V našom konkrétnom
prípade zatiaľ zostanú prázdne.
Pokrytie triedy testy
Na druhú stranu metódysetUp(self)
a
tearDown(self)
, sa zavolajú pred, resp. po
každom teste v tejto triede. To je pre nás veľmi
dôležité, pretože podľa best practices chceme, aby boli testy
nezávislé. Obvykle teda pred každým testom pripravujeme
znova to isté prostredie, aby sa vzájomne vôbec neovplyvňovali. O dobrých
praktikách sa zmienime detailnejšie neskôr. V metóde
setUp(self)
si vždy vytvorme čerstvo novú kalkulačku pre
každý test. Ak by ju bolo ešte potrebné ďalej nastavovať, alebo by bolo
treba vytvoriť ďalšie závislosti, boli by aj v tejto metóde:
# import testovacího modulu import unittest # import třídy Kalkulacka ze souboru kalkulacka.py from kalkulacka import Kalkulacka # vytvoření testovací třídy, která dědí ze třídy TestCase class TestKalkulacka(unittest.TestCase): @classmethod def setUpClass(cls): pass # volá se před začátkem všech testů @classmethod def tearDownClass(cls): pass # volá se po ukončení všech testů def setUp(self): pass # volá se před začátkem každého testu def tearDown(self): # volá se po ukončení každého testu # v tomto případě vytvoří vždy novou kalkulačku from kalkulacka import Kalkulacka Kalkulacka = Kalkulacka() ...
Máme všetko pripravené na pridávanie samotných testov. Aby testy
automaticky prebehli, budú názvy jednotlivých metód vždy začínať slovom
test. Každá funkcia bude testovať jednu konkrétnu metódu z
triedy Kalkulacka
, typicky pre niekoľko rôznych vstupov.
Pridajme nasledujúcich 5 metód:
def test_secti(self): # testuje funkci secti ze souboru kalkulacka.py self.assertEqual(Kalkulacka.secti(1, 1), 2) self.assertAlmostEqual(Kalkulacka.secti(3.14, -1.72), 1.42, 3) self.assertEqual(Kalkulacka.secti(1.0/3, 1.0/3), 2.0/3) def test_odecti(self): # testuje funkci odecti ze souboru kalkulacka.py self.assertEqual(Kalkulacka.odecti(1, 1), 0) self.assertEqual(Kalkulacka.odecti(3.14, -1.72), 4.86, 3) self.assertEqual(Kalkulacka.odecti(1.0/3, -1.0/3), 2.0/3) def test_vynasob(self): # testuje funkci vynasob ze souboru kalkulacka.py self.assertEqual(Kalkulacka.vynasob(1, 2), 2) self.assertAlmostEqual(Kalkulacka.vynasob(3.14, -1.72), -5.4008, 3) self.assertAlmostEqual(Kalkulacka.vynasob(1.0/3, 1.0/3), 0.111, 3) def test_vydel(self): # testuje funkci vydel ze souboru kalkulacka.py self.assertEqual(Kalkulacka.vydel(4, 2), 2) self.assertAlmostEqual(Kalkulacka.vydel(3.14, -1.72), -1.826, 3) self.assertEqual(Kalkulacka.vydel(1.0/3, 1.0/3), 1) def test_vydel_nulou(self): # testuje funkci vydel_nulou ze souboru kalkulacka.py with self.assertRaises(ValueError): Kalkulacka.vydel(2, 0)
Na porovnávanie výstupu metódy s očakávanou hodnotou vytvoríme vlastné
funkcie, v ktorých používame metódy assert()
, staticky
naimportované z balíčka unittest.TestCase
. Najčastejšie asi
použijete assertEqual()
, ktorá prijíma ako prvý parameter
aktuálnu hodnotu a ako druhý parameter hodnotu očakávanú. Toto poradie je
dobré dodržiavať, inak budete mať hodnoty vo výsledkoch testov opačne.
Ako asi viete, desatinné čísla sú v pamäti počítača reprezentované
binárne (ako inak 🙂), a to spôsobí určitú stratu ich presnosti a tiež
určité ťažkosti pri ich porovnávaní. Preto musíme v tomto prípade
použiť metódu assertAlmostEqual()
a zadať aj tretí parameter,
čo je delta, teda kladná tolerancia, o koľko sa môže
očakávaná a aktuálna hodnota líšiť, aby test stále prešiel.
Všimnite si, že skúšame rôzne vstupy. Sčítanie netestujeme len ako "1 + 1 = 2", ale skúsime celočíselné, desatinné aj negatívne vstupy, oddelene, a overíme výsledky. V niektorých prípadoch by nás mohla zaujímať aj maximálna hodnota dátových typov a podobne.
Posledný test overuje, či metóda vydel()
naozaj vyvolá
výnimku pri nulovom deliteľovi. Ako vidíte, nemusíme sa zaťažovať s
try-except
blokmi, do kódu stačí iba pridať
with assertRaises
(názov očakávanej chyby). Ak výnimka
nenastane, test zlyhá. Na testovanie viacerých prípadov vyvolania výnimky
týmto spôsobom by bolo potrebné pridať viac metód. Na testovanie výnimiek
sa ešte vrátime nabudúce.
Dostupné assert()
metódy
Okrem metódy assertEqual()
resp. assertAlmostEqual()
môžeme použiť ešte niekoľko ďalších. Určite sa snažte použiť tú
najviac vyhovujúcu metódu, sprehľadňuje to hlášky pri zlyhaní testov a
samozrejme aj následnú opravu:
assertEqual(a, b)
: testuje, či sa hodnotaa
rovná hodnoteb
(a = b
)assertNotEqual(a, b)
: testuje, či sa hodnotaa
nerovná hodnoteb
(a != b
)assertTrue(x)
: testuje, či booleanovská hodnota (x
) vyjde akoTrue
assertFalse(x)
: testuje, či booleanovská hodnota (x
) vyjde akoFalse
assertIs(a, b)
: testuje, či prvá a druhá hodnota sú rovnaký objektassertIsNot(a, b)
: testuje, či prvá a druhá hodnota nie sú rovnaký objektassertIsNone(x)
: testuje, či sa hodnotax
rovnáNone
assertIsNotNone(x)
: testuje, či sa hodnotax
nerovnáNone
assertIn(a, b)
: testuje, či sa hodnotaa
nachádza vb
(či je člena
kontajnerib
)assertNotIn(a, b)
: testuje, či sa hodnotaa
nenachádza vb
(či člena
nie je v kontajnerib
)assertIsInstance(a, b)
: testuje, či je objekta
inštanciou triedyb
assertNotIsInstance(a, b)
: testuje, či objekta
nie je inštanciou triedyb
Spustenie testov
Testy spustíme z terminálu zadaním príkazu:python -m unittest
Python spustí všetky súbory z adresára, ktoré začínajú slovom "test".
Pokiaľ chceme spustiť určitý súbor, zadáme jeho konkrétny názov. V
prípade požiadavky na podrobnejší výpis výsledkov doplníme aj
špecifikáciu režimu –v
(angl. v erbose =
ukecaný):
python -m unittest –v test_kalkulacka.py
Výstup bude v tomto prípade vyzerať takto:
Windows PowerShell test_odecti (test_kalkulacka.TestKalkulacka) ... ok test_secti (test_kalkulacka.TestKalkulacka) ... ok test_vydel (test_kalkulacka.TestKalkulacka) ... ok test_vydel_nulou (test_kalkulacka.TestKalkulacka) ... ok test_vynasob (test_kalkulacka.TestKalkulacka) ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.004s OK
Skúsme teraz urobiť v testovacej funkcii chybu, napr. chybne sčítame dve čísla vo funkcii testujúcej súčet (1 + 1 = 3):
def test_secti(self): self.assertEqual(Kalkulacka.secti(1, 1), 3)
A spustite znovu naše testy:
Windows PowerShell test_odecti (test_kalkulacka.TestKalkulacka) ... ok test_secti (test_kalkulacka.TestKalkulacka) ... FAIL test_vydel (test_kalkulacka.TestKalkulacka) ... ok test_vydel_nulou (test_kalkulacka.TestKalkulacka) ... ok test_vynasob (test_kalkulacka.TestKalkulacka) ... ok ================================================================== FAIL: test_secti (test_kalkulacka.TestKalkulacka) ---------------------------------------------------------------------- Traceback (most recent call last): File "c:\python\testing\test_kalkulacka.py", line 29, in test_secti self.assertEqual(Kalkulacka.secti(1, 1), 3) AssertionError: 2 != 3 ---------------------------------------------------------------------- Ran 5 tests in 0.003s FAILED (failures=1)
Vidíme, že chyba je zachytená a sme na ňu upozornení. Môžeme kód vrátiť späť do pôvodného stavu.
V budúcej lekcii, Testovanie v Pythone - PyHamcrest a best practices , sa pozrieme na knižnicu PyHamcrest, vysvetlíme si očakávané chyby a spomenieme najdôležitejšie best practices na písanie testov.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkamiStiahnuté 165x (1.8 kB)