Lekce 2 - Testování v Pythonu - První unit test s knihovnou unittest
V minulé lekci online kurzu o testování aplikací v Pythonu, Ú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 fázemi návrhu a příslušnými testy.
V dnešním tutoriálu testování v Pythonu vytvoříme jednoduchou třídu kalkulačky, kterou vzápětí otestujeme pomocí knihovny unittest. Ukážeme si, jak napsat jednotkový (unit) test pro ověření funkčnosti metod a vyvolávání výjimek.
Při psaní jednotkových testů máme k dispozici samotný kód, který chceme testovat. Testy nicméně píšeme vždy na základě návrhu, nikoli implementace. Jinak řečeno, test vytváříme na základě očekávané funkcionality. Tato specifikace chování jednotlivých metod může přijít přímo od zákazníka (typicky to platí pro akceptační testy) nebo od programátora (architekta). 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.
Nikdy nepíšeme testy podle toho, jak je něco uvnitř naprogramováno!
Při nesprávném psaní testů podle vnitřní struktury kódu by se mohlo snadno stát, že zapomeneme na některé okrajové případy, tedy vstupy, které může metoda dostat, ale není na ně připravena. Testování s implementací ve skutečnosti nesouvisí. Vždy testujeme, zda je splněno zadání.
Nástroje pro testování v Pythonu
Nástroje pro jednotkové testování jsou založeny na společném principu: asertaci. V testech kontrolujeme pravdivost formulovaných tvrzení. Nejčastěji jde o srovnání očekávaného výstupu metody s reálným výstupem. Nebo může jít o tvrzení, kdy daná metoda s nějakými konkrétními vstupy skončí výjimkou.
Existuje několik testovacích nástrojů, které slouží k psaní jednotkových testů v Pythonu. Představme si dva nejčastěji používané:
Knihovna unittest
Součástí standardní knihovny Pythonu je knihovna
unittest, která se podobá knihovnám používaným v jiných
programovacích jazycích. V tomto případě byla inspirována
konkrétně frameworkem JUnit, používaným k testování v
Javě. Knihovna umožňuje vytvářet testovací případy rozšířením
třídy unittest.TestCase
, definovat jednotlivé testovací metody
a používat různé metody asertace k ověření očekávaných výsledků.
Její výhodou je jasně definovaná a čitelná struktura,
což na druhé straně znamená více kódu.
Framework pytest
Framework pytest je frameworkem třetí strany a je třeba jej před použitím importovat. Jeho výhodou je jednoduchost, flexibilita a možnost přizpůsobení a rozšíření pomocí pluginů. Není třeba rozšiřovat jinou třídu (na rozdíl od unittestu), což znamená ve výsledku méně kódu. To činí testování přímočarým. Díky svým vlastnostem je framework používán stejně na malé i velké projekty. Nevýhodou může být delší doba potřebná k naučení se, jak tento framework používat, a to zvláště při pokročilejších nastaveních.
V tomto tutoriálu si ukážeme použití knihovny unittest.
Jaké třídy testujeme
Unit testy testují jednotlivé metody ve třídách. Pro jistotu zopakujeme, že nemá velký smysl testovat jednoúčelové metody, například v aplikacích, které pouze získávají nebo vkládají data do databáze. Abychom byli konkrétnější, nedává velký smysl testovat metodu, jako je tato:
import sqlite3 class SpravceDatabaze: def vloz_polozku(self, nazev, cena): try: spojeni = sqlite3.connect('app_db.db') kurzor = connection.cursor() kurzor.execute("INSERT INTO item (nazev, cena) VALUES (?, ?)", (nazev, cena)) spojeni.commit() except sqlite3.DatabaseError as ex: print("Chyba při komunikaci s databází") finally: spojeni.close()
Tato metoda přidává položku do databáze. Typicky je použita jen v nějakém formuláři, a pokud by nefungovala, odhalí to akceptační testy, protože by se nová položka neobjevila v seznamu. Podobných metod může být v aplikaci mnoho, a zbytečně bychom tedy ztráceli čas jejich pokrýváním pomocí unit testů, protože je lze snadno ověřit jinými typy testů.
Unit testy nejčastěji najdeme u knihoven,
tedy u nástrojů, které programátor používá na více
místech, nebo dokonce ve více projektech, a měly by být tedy 100%
funkční. Možná si vzpomenete, kdy jste použili nějakou knihovnu, staženou
například z GitHubu. Velmi pravděpodobně k ní byly připojeny i testy,
které se nejčastěji vkládají do složky tests/
, oddělené od
složky s hlavním zdrojovým kódem, například src/
nebo
main/
v adresářové struktuře projektu. Pokud například
píšeme aplikaci, ve které často potřebujeme nějaké matematické
výpočty, jako je výpočet faktoriálů nebo další pravděpodobnostní
funkce, je samozřejmostí vytvořit si na tyto výpočty knihovnu. Je také
velmi dobrý nápad pokrýt takovou knihovnu testy.
Příklad – Kalkulačka
Jak asi tušíme, my si podobnou třídu vytvoříme a zkusíme si ji otestovat. Abychom se nezdržovali, vytvořme si jen jednoduchou kalkulačku, která bude umět:
- sčítat,
- odečítat,
- násobit,
- dělit.
Vytvoření projektu
V PyCharmu si vytvoříme nový projekt s názvem
unit_testy
:

V praxi by v této třídě mohly být nějaké složitější výpočty, ale
tomu se věnovat nebudeme. Do vytvořeného projektu přidejme třídu
Kalkulacka
s následující implementací:
class Kalkulacka: def secti(self, a, b): return a + b def odecti(self, a, b): return a - b def vynasob(self, a, b): return a * b def vydel(self, a, b): if b == 0: raise ValueError("Nelze dělit nulou!") return a / b
Můžeme si všimnout metody vydel()
, která obsahuje podmínku
při dělení nulou. Python (bez ohledu na typ) při dělení nulou vyvolá
výjimku ZeroDivisionError
. V tomto případě je daná situace
řešena samostatnou podmínkou, která vyvolá výjimku
ValaueError
. Tato nová výjimka s vlastním textem je
specifičtější a pomůže uživateli identifikovat problém.
Generování testů
PyCharm umožňuje vygenerovat pokrytí třídy testy pomocí knihovny
unittest. Klikneme na deklaraci třídy Kalkulacka
a stiskneme klávesovou zkratku Alt + Insert. Vybereme
možnost Test…:

V následujícím dialogovém okně vidíme výchozí název souboru i třídy. Kromě toho lze zvolit automatické vygenerování testů pro jednotlivé metody:

Vygenerovaný kód pro jednotlivé metody obsahuje volání
self.fail()
, které způsobí selhání testu. Tento kód by měl
být nahrazen požadovanými voláními, která si ukážeme později. Můžeme
si však předtím vyzkoušet spuštění jednotlivých testů, případně
všech testů najednou:

Pokrytí třídy testy
Jeden test třídy (testovací scénář) je reprezentován také
třídou (v našem případě se jmenuje
TestKalkulacka
). Jednotlivé testy jsou tvořeny metodami, které
lze spustit. V knihovně unittest třída TestKalkulacka
dědí od třídy unittest.TestCase
.
V případě potřeby je možné vložit kód, který se provede
před spuštěním každého testu. To se realizuje překrytím
metody s názvem setUp(self)
. Podobně překrytí metody
tearDown(self)
umožní provést kód po skončení
testu. V našem případě si můžeme vytvořit nový objekt
kalkulačky vždy před provedením testu, aby se zaručila nezávislost
jednotlivých testů:
from unittest import TestCase from Kalkulacka import Kalkulacka class TestCalculator(TestCase): def setUp(self): self.kalkulacka = Kalkulacka() ...
Upravíme vygenerovaný kód, abychom měli nějaké testovací případy pro každou z metod. Navíc doplníme kontrolu metody dělení, zda skončí výjimkou v případě dělení nulou.
Metody s kódem testů v knihovně unittest musí začínat prefixem
test_
. Jinak je nebude možné spouštět:
def test_scitani(self): self.assertEqual(2, self.kalkulacka.secti(1, 1)) self.assertAlmostEqual(1.42, self.kalkulacka.secti(3.14, -1.72), places=3) self.assertAlmostEqual(2.0 / 3, self.kalkulacka.secti(1.0 / 3, 1.0 / 3), places=3) def test_odcitani(self): self.assertEqual(0, self.kalkulacka.odecti(1, 1)) self.assertAlmostEqual(4.86, self.kalkulacka.odecti(3.14, -1.72), places=3) self.assertAlmostEqual(2.0 / 3, self.kalkulacka.odecti(1.0 / 3, -1.0 / 3), places=3) self.assertFalse(2 == 8) def test_nasobeni(self): self.assertEqual(2, self.kalkulacka.vynasob(1, 2)) self.assertAlmostEqual(-5.4008, self.kalkulacka.vynasob(3.14, -1.72), places=4) self.assertAlmostEqual(0.111, self.kalkulacka.vynasob(1.0 / 3, 1.0 / 3), places=3) def test_deleni(self): self.assertEqual(2, self.kalkulacka.vydel(4, 2)) self.assertAlmostEqual(-1.826, self.kalkulacka.vydel(3.14, -1.72), places=3) self.assertEqual(1, self.kalkulacka.vydel(1.0 / 3, 1.0 / 3)) def test_deleni_vyjimka(self): with self.assertRaises(ValueError): self.kalkulacka.vydel(2, 0)
K porovnávání výstupu metody s očekávanou hodnotou
používáme metody assert*()
. Nejčastěji se jedná o metodu
assertEqual()
, která porovnává dvě vložené hodnoty. První je
očekávaná hodnota a následně skutečná. Toto pořadí je vhodné zachovat,
aby byl výpis po spuštění správný.
Desetinná čísla by se neměla srovnávat na přesnou shodu.
Desetinná čísla jsou v paměti počítače uchovávána jiným způsobem
než čísla celá. Při floating point aritmetice dochází ke ztrátě
přesnosti z důvodu chyby při zaokrouhlování nebo limitace přesnosti.
Taková čísla je proto třeba srovnávat s jistou tolerancí.
Knihovna unittest nabízí metodu assertAlmostEqual()
, kde se
definuje parametr places
, který zjednodušeně označuje, kolik
čísel za desetinnou čárkou se musí shodovat, aby byly hodnoty považovány
za stejné.
Poslední test obsahuje kontrolu, zda při dělení nulou nastane výjimka.
Metoda assertRaises()
selže v případě, že deklarovaná
výjimka nenastane.
Dostupné assert*()
metody
Kromě metody assertEqual(a, b)
můžeme dle potřeby použít i
několik dalších. Vyjmenujme si některé z nich:
assertEqual(a, b)
,assertNotEqual(a, b)
– Kontroluje, zda se hodnoty rovnají (operátor==
), resp. nerovnají.assertListEqual(a, b)
,assertSetEqual(a, b)
,assertTupleEqual(a, b)
– Kontroluje, zda se dvě kolekce (seznam, množina) shodují.assertTrue(x)
,assertFalse(x)
– Kontroluje, zda je výraz pravdivý (True
), resp. nepravdivý (False
).assertIsNone(x)
,assertIsNotNone(x)
– Kontroluje, zda je (resp. není) hodnotaNone
.assertIs(a, b)
,assertIsNot(a, b)
– Kontroluje, zda jsou (resp. nejsou) dvě reference na stejný objekt (operátoris
).assertIn(a, b)
,assertNotIn(a, b)
– Kontroluje, zda je (resp. není) hodnotaa
v kolekcib
(seznam, množina).assertIsInstance(a, b)
,assertNotIsInstance(a, b)
– Kontroluje, zda jea
instancí třídyb
, resp. není.assertAlmostEqual(a, b, places)
,assertNotAlmostEqual(a, b, places)
– Kontroluje, zda jsou dvě hodnoty stejné s přesností na zadaný počet desetinných míst.assertGreater(a, b)
,assertGreaterEqual(a, b)
,assertLess(a, b)
,assertLessEqual(a, b)
– Porovnává dvě hodnoty.assertRaises(exception)
– Kontroluje vyhození výjimky.
Spuštění testů
Testy lze v PyCharmu spustit kliknutím na příslušný soubor a možností Run Python tests in test… nebo pomocí zeleného trojúhelníku. Spuštění testování zobrazí průběh testu a výsledky:

Zkusme si kalkulačku upravit následovně:
def vydel(self, a, b): # if b == 0: # raise ValueError("Nelze dělit nulou!") return a / b
Po spuštění testů vidíme zachycenou chybu:

Můžeme kód vrátit do původního stavu.
V následujícím kvízu, Kvíz - Úvod do testování a unit testů v Pythonu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 25x (5.99 kB)
Aplikace je včetně zdrojových kódů v jazyce Python