Zarábaj až 6 000 € mesačne! Akreditované rekvalifikačné kurzy od 0 €. Viac informácií.

2. diel - Testovanie v Pythone - Prvý unit test s knižnicou unittest Nové

V minulej lekcii online kurzu o testovaní aplikácií v Pythone, Ú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.

V dnešnom tutoriáli testovania v Pythone vytvoríme jednoduchú triedu kalkulačky, ktorú vzápätí otestujeme pomocou knižnice unittest. Ukážeme si ako napísať jednotkový (unit) test na overenie funkčnosti metód a vyvolávania výnimiek.

Pri písaní jednotkových testov máme k dispozícii samotný kód, ktorý chceme testovať. Avšak, testy píšeme vždy na základe návrhu, nie implementácie. Inak povedané, test vytvárame na základe očakávanej funkcionality. Táto špecifikácia správania sa jednotlivých metód môže prísť priamo od zákazníka (typicky to platí pre akceptačné testy) alebo od programátora (architekta). 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.

Nikdy nepíšeme testy podľa toho, ako je niečo vo vnútri naprogramované!

Pri nesprávnom písaní testov podľa vnútornej štruktúry kódu by sa mohlo ľahko stať, že zabudneme na niektoré okrajové prípady, čiže vstupy, ktoré môže metóda dostať, ale nie je na ne pripravená. Testovanie s implementáciou v skutočnosti nesúvisí. Vždy testujeme, či je splnené zadanie.

Nástroje na testovanie v Pythone

Nástroje na jednotkové testovanie sú založené na spoločnom príncípe, tzv. asertácii. V testoch vytvárame nejaké tvrdenia, ktoré sa kontrolujú, či sú pravdivé. Najčastejšie ide o porovnanie očakávaného výstupu metódy s reálnym výstupom. Alebo to môže byť tvrdenie, že daná metóda s nejakými konkrétnymi vstupmi skončí výnimkou.

Existuje viacero testovacích nástrojov, ktoré slúžia na písanie jednotkových testov v Pythone. Predstavme si dva najčastejšie používané:

Knižnica unittest

Súčasťí štandardnej knižnice Pythonu je knižnica unittest, ktorý sa podobá na knižnice používané v iných programovacích jazykoch. V tomto prípade bol inšpirovaný konkrétne frameworkom JUnit používaným na testovanie v Jave. Umožňuje vytvárať testovacie prípady rozšírením triedy unittest.TestCase, definovať jednotlivé testovacie metódy a používať rôzne metódy asertácie na overenie očakávaných výsledkov. Jej výhodou je jasne definovaná a čitateľná štruktúra, čo na druhej strane znamená viac kódu.

Framework pytest

Framework pytest je frameworkom tretej strany a je potrebné ho pred použitím importovať. Jeho výhodou je jednoduchosť, flexibilita a možnosť prispôsobenia a rozšírenia pomocou pluginov. Nie je potrebné rozširovať inú triedu (na rozdiel od unittestu), čo znamená vo výsledku menej kódu a robí to testovanie priamočiarým. Vďaka svojím vlastnostiam je používaný rovnako na malé aj na veľké projekty. Nevýhodou môže byť dlhší čas potrebný na naučenie sa, ako používať tento framework, zvlášť pri pokročilejších nastaveniach.

V tomto tutoriáli si ukážeme použitie knižnice unittest.

Aké triedy testujeme

Unit testy testujú jednotlivé metódy v triedach. Pre istotu zopakujem, že nemá veľký zmysel testovať jednoúčelové metódy, napríklad v aplikáciách, ktoré iba získavajú alebo vkladajú dáta do databázy. Aby sme boli konkrétnejší, nemá veľký zmysel testovať metódu, ako je táto:

import sqlite3

class DatabaseManager:
    def insert_item(self, name, price):
        try:
            connection = sqlite3.connect('app_db.db')
            cursor = connection.cursor()
            cursor.execute("INSERT INTO item (name, price) VALUES (?, ?)", (name, price))
            connection.commit()
        except sqlite3.DatabaseError as ex:
            print("Error while communicating with the database")
        finally:
            connection.close()

Táto metóda pridáva položku do databázy. Typicky je použitá len v nejakom formulári a pokiaľ by nefungovala, odhalia to akceptačné testy, pretože by sa nová položka neobjavila v zozname. Podobných metód môže byť v aplikácii mnoho, a zbytočne by sme strácali čas ich pokrývaním pomocou unit testov, pretože sa dajú ľahko overiť v iných typoch testov.

Unit testy najčastejšie nájdeme 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íklad z GitHubu. Veľmi pravdepodobne u nej boli aj testy, ktoré sa najčastejšie vkladajú do zložky tests/, oddelené od zložky s hlavným zdrojovým kódom, napríklad src/ alebo main/ v adresárovej štruktúre projektu. Ak napríklad píšete aplikáciu, v ktorej často potrebujete nejaké matematické výpočty, ako výpočet faktoriálov alebo ď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 - Kalkulačka

Ako asi tušíme, 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ť:

  • spočítať,
  • odpočítať,
  • vynásobiť,
  • deliť.

Vytvorenie projektu

V Pycharm si vytvoríme nový projekt s názvom unit_tests:

Vytvorenie projektu v Pycharm - Testovanie v Pythone

V praxi by v tejto triede mohli byť nejaké zložitejšie výpočty, ale tomu sa venovať nebudeme. Do vytvoreného projektu pridajme triedu Calculator s nasledujúcou implementáciou:

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero!")
        return a / b

Môžeme si všimnúť metódu divide(), ktorá obsahuje podmienku pri delení nulou. Python (bez ohľadu na typ) pri delení nulou vyvolá výnimku ZeroDivisionError. V tomto prípade je daná situácia riešená samostatnou podmienkou, ktorá vyvolá výnimku ValaueError. Táto nová výnimka s vlastným textom je viac špecifická a pomôže používateľovi identifikovať problém.

Generovanie testov

Pycharm umožňuje vygenerovať pokrytie triedy testami pomocou knižnice unittest. Klikneme na deklaráciu triedy Calculator a stlačíme klávesovú skratku Alt + Insert. Vyberieme možnosť Test...:

Generovanie unit testov v Pycharm - Testovanie v Pythone

V nasledujúcom dialógovom okne vidíme predvolený názov súboru aj triedy. Okrem toho je možné zvoliť automatické vygenerovanie testov pre jednotlivé metódy:

Create test - Testovanie v Pythone

Vygenerovaný kód pre jednotlivé metódy obsahuje volanie self.fail(), ktoré spôsobí zlyhanie testu. Tento kód by mal byť nahradený požadovanými volaniami, ktoré si ukážeme neskôr. Môžeme si však predtým vyskúšať spustenie jednodlivých testov, prípadne všetkých testov naraz:

Spuštenie testov - Testovanie v Pythone

Pokrytie triedy testami

Jeden test triedy (testovací scenár) je reprezentovaný tiež triedou (v našom prípade sa volá TestCalculator). Jednotlivé testy sú tvorené metódami, ktoré sa dajú spustiť. V knižnici unittest trieda TestCalculator dedí od triedy unittest.TestCase.

V prípade potreby je možné vložiť kód, ktorý sa vykoná pred spustením každého testu. To sa realizuje prekrytím metódy s názvom setUp(self). Podobne prekrytie metódy tearDown(self) umožní vykonať kód po skončení testu. V našom prípade si môžeme vytvoriť nový objekt kalkulačky vždy pred vykonaním testu, aby sa zaručila nezávislosť jednotlivých testov:

from unittest import TestCase

from Calculator import Calculator


class TestCalculator(TestCase):

    def setUp(self):
        self.calculator = Calculator()

    ...

Upravíme vygenerovaný kód, aby sme mali nejaké testovacie prípady pre každú z metód. Navyše doplníme kontrolu metódy delenia, či skončí výnimkou v prípade delenia nulou.

Metódy s kódom testov v knižnici unittest musia začínať prefixom test_. Inak ich nebude možné spúšťať:

def test_addition(self):
    self.assertEqual(2, self.calculator.add(1, 1))
    self.assertAlmostEqual(1.42, self.calculator.add(3.14, -1.72), places=3)
    self.assertAlmostEqual(2.0 / 3, self.calculator.add(1.0 / 3, 1.0 / 3), places=3)

def test_subtraction(self):
    self.assertEqual(0, self.calculator.subtract(1, 1))
    self.assertAlmostEqual(4.86, self.calculator.subtract(3.14, -1.72), places=3)
    self.assertAlmostEqual(2.0 / 3, self.calculator.subtract(1.0 / 3, -1.0 / 3), places=3)
    self.assertFalse(2 == 8)

def test_multiplication(self):
    self.assertEqual(2, self.calculator.multiply(1, 2))
    self.assertAlmostEqual(-5.4008, self.calculator.multiply(3.14, -1.72), places=4)
    self.assertAlmostEqual(0.111, self.calculator.multiply(1.0 / 3, 1.0 / 3), places=3)

def test_division(self):
    self.assertEqual(2, self.calculator.divide(4, 2))
    self.assertAlmostEqual(-1.826, self.calculator.divide(3.14, -1.72), places=3)
    self.assertEqual(1, self.calculator.divide(1.0 / 3, 1.0 / 3))

def test_division_exception(self):
    with self.assertRaises(ValueError):
        self.calculator.divide(2, 0)

K porovnávaniu výstupu metódy s očakávanou hodnotou používame metódy assert*(). Najčastejšie ide o metódu assertEqual(), ktorá porovnáva dve vložené hodnoty. Prvá ide očákávana hodnota a následne skutočná. Toto poradie je vhodné zachovať, aby bol výpis po spustení správny.

Desatinné čísla by sa nemali porovnávať na presnú zhodu.

Desatinné čísla sú v pamäti počítača uchovávané iným spôsobom ako celé čísla. Pri floating point aritmetike dochádza k strate presnosti z dôvodu chyby pri zaokrúhľovaní alebo limitáciou presnosti. Je preto potrebné takéto čísla porovnávať s istou toleranciou. Knižnica unittest ponúka metódu assertAlmostEqual(), kde sa definuje parameter places, ktorý zjednodušene označuje, koľko čísel za desatinnou čiarkou sa musí zhodovať, aby boli hodnoty považované za rovnaké.

Posledný test obsahuje kontrolu, či pri delení nulou nastane výnimka. Metóda assertRaises() zlyhá v prípade, že deklarovaná výnimka nenastane.

Dostupné assert*() metódy

Okrem metódy assertEqual(a, b) môžeme použiť aj niekoľko ďalších podľa potreby. Vymenujme si niektoré z nich:

  • assertEqual(a, b), assertNotEqual(a, b) - kontroluje, či sa hodnoty rovnajú (operátor ==), resp. nerovnajú.
  • assertListEqual(a, b), assertSetEqual(a, b), assertTupleEqual(a, b) - kontroluje, či sa dve kolekcie (zoznam, množina) zhodujú.
  • assertTrue(x), assertFalse(x) - kontroluje, či je výraz pravdivý (True), resp. nepravdivý (False).
  • assertIsNone(x), assertIsNotNone(x) - kontroluje, či je (resp. nie je) hodnota None.
  • assertIs(a, b), assertIsNot(a, b) - kontroluje, či sú (resp. nie sú) dve referencie na rovnaký objekt (operátor is).
  • assertIn(a, b), assertNotIn(a, b) - kontroluje, či je (resp. nie je) hodnota a v kolekcii b (zoznam, množina).
  • assertIsInstance(a, b), assertNotIsInstance(a, b) - kontroluje, či je a inštanciou triedy b, resp. nie je.
  • assertAlmostEqual(a, b, places), assertNotAlmostEqual(a, b, places) - kontroluje, či sú dve hodnoty rovnaké s presnosťou na zadaný počet desatinných miest.
  • assertGreater(a, b), assertGreaterEqual(a, b), assertLess(a, b), assertLessEqual(a, b) - porovnáva dve hodnoty.
  • assertRaises(exception) - kontroluje vyhodenie výnimky.

Spustenie testov

Testy je možné spustiť v Pycharm kliknutím na príslušný súbor a možnosťou Run Python tests in test... alebo pomocou zeleného trojuholníka. Spustenie testovania ukáže priebeh testu a výsledky:

Výsledky testov - Testovanie v Pythone

Skúsme si upraviť kalkulačku nasledovne:

def divide(self, a, b):
    # if b == 0:
    #     raise ValueError("Cannot divide by zero!")
    return a / b

Po spustení testov vidíme zachytenú chybu:

Neúspešný výsledok testov - Testovanie v Pythone

Môžeme kód vrátiť do pôvodného stavu.

V nasledujúcom kvíze, Kvíz - Úvod do testovania a unit testov v Pythone, si vyskúšame nadobudnuté skúsenosti z predchádzajúcich lekcií.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 9x (2.38 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python

 

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)
Kvíz - Úvod do testovania a unit testov v Pythone
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity