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
:
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...:
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:
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:
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) hodnotaNone
.assertIs(a, b)
,assertIsNot(a, b)
- kontroluje, či sú (resp. nie sú) dve referencie na rovnaký objekt (operátoris
).assertIn(a, b)
,assertNotIn(a, b)
- kontroluje, či je (resp. nie je) hodnotaa
v kolekciib
(zoznam, množina).assertIsInstance(a, b)
,assertNotIsInstance(a, b)
- kontroluje, či jea
inštanciou triedyb
, 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:
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:
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é 8x (2.38 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python