Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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ázvom kalkulacka.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ódy setUp(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 hodnota a rovná hodnote b (a = b)
  • assertNotEqual(a, b): testuje, či sa hodnota a nerovná hodnote b (a != b)
  • assertTrue(x): testuje, či booleanovská hodnota (x) vyjde ako True
  • assertFalse(x): testuje, či booleanovská hodnota (x) vyjde ako False
  • assertIs(a, b): testuje, či prvá a druhá hodnota sú rovnaký objekt
  • assertIsNot(a, b): testuje, či prvá a druhá hodnota nie sú rovnaký objekt
  • assertIsNone(x): testuje, či sa hodnota x rovná None
  • assertIsNotNone(x): testuje, či sa hodnota x nerovná None
  • assertIn(a, b): testuje, či sa hodnota a nachádza v b (či je člen a kontajneri b)
  • assertNotIn(a, b): testuje, či sa hodnota a nenachádza v b (či člen a nie je v kontajneri b)
  • assertIsInstance(a, b): testuje, či je objekt a inštanciou triedy b
  • assertNotIsInstance(a, b): testuje, či objekt a nie je inštanciou triedy b

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 podmienkami

Stiahnuté 165x (1.8 kB)

 

Predchádzajúci článok
Úvod do testovania softvéru v Pythone
Všetky články v sekcii
Testovanie v Pythone
Preskočiť článok
(neodporúčame)
Testovanie v Pythone - PyHamcrest a best practices
Článok pre vás napísal Patrik Bernat
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity