Lekce 2 - Testování v Pythonu - První unit test v unittest frameworku
V minulé lekci, Úvod do testování softwaru v Pythonu, jsme si udělali poměrně solidní úvod do problematiky. Také jsme si uvedli V-model, který znázorňuje vztah mezi jednotlivými výstupy fází návrhu a příslušnými testy.
Testy tedy píšeme vždy na základě návrhu, nikoli implementace. Jinými slovy, děláme je na základě očekávané funkčnosti. Ta může být buď přímo od zákazníka (a to v případě akceptačních testů) nebo již od programátora (architekta), kde specifikuje, jak se má která metoda chovat. Dnes se budeme věnovat právě těmto testům, kterým říkáme jednotkové (unit testy) a které testují detailní specifikaci aplikace, tedy její třídy.
Pamatujte, že nikdy nepíšeme testy podle toho, jak je něco uvnitř naprogramované! Velmi jednoduše by to mohlo naše myšlení svést jen tím daným způsobem a zapomněli bychom na to, že metodě mohou přijít třeba i jiné vstupy, na které není vůbec připravená. Testování s implementací ve skutečnosti vůbec nesouvisí, vždy testujeme, zda je splněno zadání.
Jaké třídy testujeme
Unit testy testují jednotlivé metody ve třídách. Pro jistotu zopakuji, že nemá valný smysl testovat jednoúčelové metody, které např. pouze něco vybírají z databáze. Abychom byli konkrétnější, nemá smysl testovat metodu, jako je tato:
import sqlite3 as db def vloz_polozku(nazev, cena): conn = db.connect('aplikace.db') cur = conn.cursor() try: cur.execute('INSERT INTO polozka (nazev, cena) VALUES (?, ?)', (nazev, cena)) except db.OperationalError: print('Chyba při komunikaci s databází') cur.close() conn.commit() conn.close()
Metoda přidává položku do databáze. Typicky je použita jen v nějakém formuláři a pokud by nefungovala, zjistí to akceptační testy, jelikož by se nová položka neobjevila v seznamu. Podobných metod je v aplikaci hodně a zbytečně bychom ztráceli čas pokrýváním něčeho, co snadno pokryjeme v jiných testech.
Unit testy nalezneme nejčastěji u knihoven, tedy
nástrojů, které programátor používá na více místech
nebo dokonce ve více projektech a měly by být 100% funkční. Možná si
vzpomenete, kdy jste použili nějakou knihovnu, staženou např. z GitHubu.
Velmi pravděpodobně u ní byly také testy, které se nejčastěji vkládají
do složky test/
, která je vedle složky src/
v
adresářové struktuře projektu. Pokud např. píšeme aplikaci, ve které
často potřebujeme nějaké matematické výpočty, např. faktoriály a
další pravděpodobnostní funkce, je samozřejmostí vytvořit si na tyto
výpočty knihovnu a je velmi dobrý nápad pokrýt takovou knihovnu testy.
Příklad
Jak asi tušíte, my si podobnou třídu vytvoříme a zkusíme si ji otestovat. Abychom se nezdržovali, vytvořme si pouze jednoduchou kalkulačku, která bude umět:
- sčítat
- odčítat
- násobit
- dělit
Vytvoření projektu
V praxi by ve třídě byly nějaké složitější výpočty, ale tím se
zde zabývat nebudeme. Vytvořme si nový soubor s názvem
kalkulacka.py
a do něj si přidejme třídu
Kalkulacka
a následující implementaci:
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ódu je zajímavá metoda vydel()
, která vyvolá výjimku v
případě, že dělíme nulou.
Generování testů
V Pythonu pro testování jednotek (unit testing) používáme framework s příhodným názvem unittest, který byl původně inspirován JUnit z jazyku Java a je již součástí základního balíčku knihoven, nemusíme tedy nic instalovat.
Testy většinou nepíšeme do stejného souboru, ale pro přehlednost
vložíme kód do nového souboru, který podle konvence pojmenujeme
test_název, např. test_kalkulacka.py
.
V první řadě si naimportujeme modul unittest
a třídu
Kalkulacka
ze souboru, který budeme testovat, tedy
kalkulacka.py
. Poté si vytvoříme testovací třídu, která je
odvozena od třídy TestCase
testovacího modulu
unittest
a zdědí z ní řadu užitečných metod, které později
využijeme. Asi nás v objektovém Pythonu nepřekvapí, že je test třídy
(scénář) reprezentovaný také třídou a jednotlivé testy metodami.
Do třídy je zvykem přidat classmethod s názvem
setUpClass(cls)
, která se zavolá jednou na začátku před všemi
testy. Přidáme další metodu s názvem tearDownClass(cls)
,
která se zavolá jednou na konci, poté co testy proběhnou. V našem
konkrétním případě zůstanou zatím prázdné.
Pokrytí třídy testy
Na druhou stranu, metody setUp(self)
a
tearDown(self)
se zavolají před, resp. po
každém testu v této třídě. To je pro nás velmi důležité,
jelikož podle best practices chceme, aby byly testy
nezávislé. Obvykle tedy před každým testem připravujeme
znovu to samé prostředí, aby se vzájemně vůbec neovlivňovaly. O dobrých
praktikách se zmíníme detailněji později. V metodě
setUp(self)
si vždy vytvořme čerstvě novou kalkulačku pro
každý test. Pokud by ji bylo ještě třeba dále nastavovat nebo by bylo
třeba vytvořit další závislosti, byly by také v této metodě
setUp(self)
:
# 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še připraveno k přidávání samotných testů. Aby testy
automaticky proběhly, budou názvy jednotlivých metod vždy začínat slovem
test. Každá funkce bude testovat jednu konkrétní metodu ze
třídy Kalkulacka
, typicky pro několik různých vstupů.
Přidejme následujících 5 metod:
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)
K porovnávání výstupu metody s očekávanou hodnotou vytvoříme vlastní
funkce, ve kterých používáme metody assert()
. Ty jsou staticky
naimportované z balíčku unittest.TestCase
. Nejčastěji asi
použijeme assertEqual()
, která přijímá jako první parametr
aktuální hodnotu a jako druhý parametr hodnotu očekávanou. Toto pořadí je
dobré dodržovat, jinak budeme mít hodnoty ve výsledcích testů
opačně.
Jak asi víme, desetinná čísla jsou v paměti počítače reprezentována
binárně (jak jinak 🙂), a to způsobí určitou ztrátu jejich přesnosti a
také určité obtíže při jejich porovnávání. Proto musíme v tomto
případě použít metodu assertAlmostEqual()
a zadat i třetí
parametr, což je delta, tedy kladná tolerance, o kolik se
může očekávaná a aktuální hodnota lišit, aby test stále prošel.
Všimněme si, že zkoušíme různé vstupy. Sčítání netestujeme jen
jako 1 + 1 = 2
, ale zkusíme celočíselné, desetinné i
negativní vstupy, odděleně, a ověříme výsledky. V některých případech
by nás mohla zajímat také maximální hodnota datových typů a podobně.
Poslední test ověřuje, zda metoda vydel()
opravdu vyvolá
výjimku při nulovém děliteli. Jak můžeme vidět, nemusíme se zatěžovat
s try-except
bloky, do kódu stačí pouze přidat
with assertRaises
(název očekávané chyby). Pokud výjimka
nenastane, test selže. Pro testování více případů vyvolání výjimky
tímto způsobem by bylo třeba přidat více metod. K testování výjimek se
ještě vrátíme příště.
Dostupné assert metody
Kromě metod assertEqual()
a assertAlmostEqual()
můžeme použít ještě několik dalších. Určitě se snažte použít tu
nejvíce vyhovující metodu, zpřehledňuje to hlášky při selhání testů a
samozřejmě i následnou opravu:
assertEqual(a, b)
- zkontroluje, zda se hodnotaa
rovná hodnotěb
(a = b
)assertNotEqual(a, b)
- zkontroluje, zda se hodnotaa
nerovná hodnotěb
(a != b
)assertTrue(x)
- zkontroluje, zda booleanovská hodnota (x
) vyjde jakoTrue
assertFalse(x)
- zkontroluje, zda booleanovská hodnota (x
) vyjde jakoFalse
assertIs(a, b)
- zkontroluje, zda první a druhá hodnota jsou stejný objektassertIsNot(a, b)
- zkontroluje, zda první a druhá hodnota nejsou stejný objektassertIsNone(x)
- zkontroluje, zda se hodnotax
rovnáNone
assertIsNotNone(x)
- zkontroluje, zda se hodnotax
nerovnáNone
assertIn(a, b)
- zkontroluje, zda se hodnotaa
nachází vb
(zda je člena
v kontejnerub
)assertNotIn(a, b)
- zkontroluje, zda se hodnotaa
nenachází vb
(zda člena
není v kontejnerub
)assertIsInstance(a, b)
- zkontroluje, zda je objekta
instancí třídyb
assertNotIsInstance(a, b)
- zkontroluje, zda objekta
není instancí třídyb
Spuštění testů
Testy spustíme z terminálu zadáním příkazu:
python -m unittest
Python spustí všechny soubory z adresáře, které začínají slovem
"test". Pokud chceme spustit určitý soubor, zadáme jeho konkrétní název na
konec příkazu. V případě požadavku na podrobnější výpis výsledků
doplníme i specifikaci režimu –v
(angl.
verbose = upovídaný):
python -m unittest –v test_kalkulacka.py
Výstup bude v tomto případě vypadat 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
Zkusme nyní udělat v kalkulačce chybu, např. zakomentujme vyvolávání výjimky při dělení nulou:
def vydel(a, b): #if b == 0: # raise ValueError('Nelze dělit nulou!') return a / b
A spusťme znovu naše testy:
Windows PowerShell ====================================================================== ERROR: test_vydel_nulou (test_kalkulacka.TestKalkulacka) ---------------------------------------------------------------------- Traceback (most recent call last): File "c:\python\testing\test_kalkulacka.py", line 54, in test_vydel_nulou Kalkulacka.vydel(2, 0) File "c:\python\testing\kalkulacka.py", line 23, in vydel return a / b ZeroDivisionError: division by zero ---------------------------------------------------------------------- Ran 5 tests in 0.005s FAILED (errors=1)
Vidíme, že chyba je zachycena a jsme na ni upozorněni. Můžeme kód vrátit zpět do původního stavu.
V příští lekci, Testování v Pythonu - PyHamcrest a best practices, se podíváme na knihovnu PyHamcrest, vysvětlíme si očekávané chyby a zmíníme nejdůležitější best practices pro psaní testů.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkamiStaženo 166x (1.8 kB)