NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
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í.

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:

Vytvoření projektu v PyCharmu - Testování v Pythonu

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…:

Generování unit testů v PyCharmu - Testování v Pythonu

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:

Create test - Testování v Pythonu

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:

Spuštění testů - Testování v Pythonu

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í) hodnota None.
  • assertIs(a, b), assertIsNot(a, b) – Kontroluje, zda jsou (resp. nejsou) dvě reference na stejný objekt (operátor is).
  • assertIn(a, b), assertNotIn(a, b) – Kontroluje, zda je (resp. není) hodnota a v kolekci b (seznam, množina).
  • assertIsInstance(a, b), assertNotIsInstance(a, b) – Kontroluje, zda je a instancí třídy b, 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:

Výsledky testů - Testování v Pythonu

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:

Neúspěšný výsledek testů - Testování v Pythonu

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

 

Předchozí článek
Úvod do testování softwaru v Pythonu
Všechny články v sekci
Testování v Pythonu
Přeskočit článek
(nedoporučujeme)
Kvíz - Úvod do testování a unit testů v Pythonu
Článek pro vás napsal David Hartinger
Avatar
Uživatelské hodnocení:
23 hlasů
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 se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity