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í.

Lekce 2 - Testování v Pythonu - První unit test v unittest frameworku

V minulé lekci, Ú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 výstupy fází návrhu a příslušnými testy.

Testy tedy píšeme vždy na základě návrhu, nikoli implementace. Jinými slovy, děláme je na základě očekávané funkčnosti. Ta může být buď přímo od zákazníka (a to v případě akceptačních testů) nebo již od programátora (architekta), kde specifikuje, jak se má která metoda chovat. 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.

Pamatujte, že nikdy nepíšeme testy podle toho, jak je něco uvnitř naprogramované! Velmi jednoduše by to mohlo naše myšlení svést jen tím daným způsobem a zapomněli bychom na to, že metodě mohou přijít třeba i jiné vstupy, na které není vůbec připravená. Testování s implementací ve skutečnosti vůbec nesouvisí, vždy testujeme, zda je splněno zadání.

Jaké třídy testujeme

Unit testy testují jednotlivé metody ve třídách. Pro jistotu zopakuji, že nemá valný smysl testovat jednoúčelové metody, které např. pouze něco vybírají z databáze. Abychom byli konkrétnější, nemá smysl testovat metodu, jako je tato:

import sqlite3 as db

def vloz_polozku(nazev, cena):
    conn = db.connect('aplikace.db')
    cur = conn.cursor()
    try:
        cur.execute('INSERT INTO polozka (nazev, cena) VALUES (?, ?)', (nazev, cena))
    except db.OperationalError:
        print('Chyba při komunikaci s databází')
    cur.close()
    conn.commit()
    conn.close()

Metoda přidává položku do databáze. Typicky je použita jen v nějakém formuláři a pokud by nefungovala, zjistí to akceptační testy, jelikož by se nová položka neobjevila v seznamu. Podobných metod je v aplikaci hodně a zbytečně bychom ztráceli čas pokrýváním něčeho, co snadno pokryjeme v jiných testech.

Unit testy nalezneme nejčastěji u knihoven, tedy nástrojů, které programátor používá na více místech nebo dokonce ve více projektech a měly by být 100% funkční. Možná si vzpomenete, kdy jste použili nějakou knihovnu, staženou např. z GitHubu. Velmi pravděpodobně u ní byly také testy, které se nejčastěji vkládají do složky test/, která je vedle složky src/ v adresářové struktuře projektu. Pokud např. píšeme aplikaci, ve které často potřebujeme nějaké matematické výpočty, např. faktoriály a další pravděpodobnostní funkce, je samozřejmostí vytvořit si na tyto výpočty knihovnu a je velmi dobrý nápad pokrýt takovou knihovnu testy.

Příklad

Jak asi tušíte, my si podobnou třídu vytvoříme a zkusíme si ji otestovat. Abychom se nezdržovali, vytvořme si pouze jednoduchou kalkulačku, která bude umět:

  • sčítat
  • odčítat
  • násobit
  • dělit

Vytvoření projektu

V praxi by ve třídě byly nějaké složitější výpočty, ale tím se zde zabývat nebudeme. Vytvořme si nový soubor s názvem kalkulacka.py a do něj si přidejme třídu Kalkulacka a následující implementaci:

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ódu je zajímavá metoda vydel(), která vyvolá výjimku v případě, že dělíme nulou.

Generování testů

V Pythonu pro testování jednotek (unit testing) používáme framework s příhodným názvem unittest, který byl původně inspirován JUnit z jazyku Java a je již součástí základního balíčku knihoven, nemusíme tedy nic instalovat.

Testy většinou nepíšeme do stejného souboru, ale pro přehlednost vložíme kód do nového souboru, který podle konvence pojmenujeme test_název, např. test_kalkulacka.py.

V první řadě si naimportujeme modul unittest a třídu Kalkulacka ze souboru, který budeme testovat, tedy kalkulacka.py. Poté si vytvoříme testovací třídu, která je odvozena od třídy TestCase testovacího modulu unittest a zdědí z ní řadu užitečných metod, které později využijeme. Asi nás v objektovém Pythonu nepřekvapí, že je test třídy (scénář) reprezentovaný také třídou a jednotlivé testy metodami.

Do třídy je zvykem přidat classmethod s názvem setUpClass(cls), která se zavolá jednou na začátku před všemi testy. Přidáme další metodu s názvem tearDownClass(cls), která se zavolá jednou na konci, poté co testy proběhnou. V našem konkrétním případě zůstanou zatím prázdné.

Pokrytí třídy testy

Na druhou stranu, metody setUp(self) a tearDown(self) se zavolají před, resp. po každém testu v této třídě. To je pro nás velmi důležité, jelikož podle best practices chceme, aby byly testy nezávislé. Obvykle tedy před každým testem připravujeme znovu to samé prostředí, aby se vzájemně vůbec neovlivňovaly. O dobrých praktikách se zmíníme detailněji později. V metodě setUp(self) si vždy vytvořme čerstvě novou kalkulačku pro každý test. Pokud by ji bylo ještě třeba dále nastavovat nebo by bylo třeba vytvořit další závislosti, byly by také v této metodě setUp(self):

# 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še připraveno k přidávání samotných testů. Aby testy automaticky proběhly, budou názvy jednotlivých metod vždy začínat slovem test. Každá funkce bude testovat jednu konkrétní metodu ze třídy Kalkulacka, typicky pro několik různých vstupů.

Přidejme následujících 5 metod:

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)

K porovnávání výstupu metody s očekávanou hodnotou vytvoříme vlastní funkce, ve kterých používáme metody assert(). Ty jsou staticky naimportované z balíčku unittest.TestCase. Nejčastěji asi použijeme assertEqual(), která přijímá jako první parametr aktuální hodnotu a jako druhý parametr hodnotu očekávanou. Toto pořadí je dobré dodržovat, jinak budeme mít hodnoty ve výsledcích testů opačně.

Jak asi víme, desetinná čísla jsou v paměti počítače reprezentována binárně (jak jinak 🙂), a to způsobí určitou ztrátu jejich přesnosti a také určité obtíže při jejich porovnávání. Proto musíme v tomto případě použít metodu assertAlmostEqual() a zadat i třetí parametr, což je delta, tedy kladná tolerance, o kolik se může očekávaná a aktuální hodnota lišit, aby test stále prošel.

Všimněme si, že zkoušíme různé vstupy. Sčítání netestujeme jen jako 1 + 1 = 2, ale zkusíme celočíselné, desetinné i negativní vstupy, odděleně, a ověříme výsledky. V některých případech by nás mohla zajímat také maximální hodnota datových typů a podobně.

Poslední test ověřuje, zda metoda vydel() opravdu vyvolá výjimku při nulovém děliteli. Jak můžeme vidět, nemusíme se zatěžovat s try-except bloky, do kódu stačí pouze přidat with assertRaises (název očekávané chyby). Pokud výjimka nenastane, test selže. Pro testování více případů vyvolání výjimky tímto způsobem by bylo třeba přidat více metod. K testování výjimek se ještě vrátíme příště.

Dostupné assert metody

Kromě metod assertEqual() a assertAlmostEqual() můžeme použít ještě několik dalších. Určitě se snažte použít tu nejvíce vyhovující metodu, zpřehledňuje to hlášky při selhání testů a samozřejmě i následnou opravu:

  • assertEqual(a, b) - zkontroluje, zda se hodnota a rovná hodnotě b (a = b)
  • assertNotEqual(a, b) - zkontroluje, zda se hodnota a nerovná hodnotě b (a != b)
  • assertTrue(x) - zkontroluje, zda booleanovská hodnota (x) vyjde jako True
  • assertFalse(x) - zkontroluje, zda booleanovská hodnota (x) vyjde jako False
  • assertIs(a, b) - zkontroluje, zda první a druhá hodnota jsou stejný objekt
  • assertIsNot(a, b) - zkontroluje, zda první a druhá hodnota nejsou stejný objekt
  • assertIsNone(x) - zkontroluje, zda se hodnota x rovná None
  • assertIsNotNone(x) - zkontroluje, zda se hodnota x nerovná None
  • assertIn(a, b) - zkontroluje, zda se hodnota a nachází v b (zda je člen a v kontejneru b)
  • assertNotIn(a, b) - zkontroluje, zda se hodnota a nenachází v b (zda člen a není v kontejneru b)
  • assertIsInstance(a, b) - zkontroluje, zda je objekt a instancí třídy b
  • assertNotIsInstance(a, b) - zkontroluje, zda objekt a není instancí třídy b

Spuštění testů

Testy spustíme z terminálu zadáním příkazu:

python -m unittest

Python spustí všechny soubory z adresáře, které začínají slovem "test". Pokud chceme spustit určitý soubor, zadáme jeho konkrétní název na konec příkazu. V případě požadavku na podrobnější výpis výsledků doplníme i specifikaci režimu –v (angl. verbose = upovídaný):

python -m unittest –v test_kalkulacka.py

Výstup bude v tomto případě vypadat 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

Zkusme nyní udělat v kalkulačce chybu, např. zakomentujme vyvolávání výjimky při dělení nulou:

def vydel(a, b):
    #if b == 0:
    #    raise ValueError('Nelze dělit nulou!')
    return a / b

A spusťme znovu naše testy:

Windows PowerShell
======================================================================
ERROR: test_vydel_nulou (test_kalkulacka.TestKalkulacka)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "c:\python\testing\test_kalkulacka.py", line 54, in test_vydel_nulou
    Kalkulacka.vydel(2, 0)
  File "c:\python\testing\kalkulacka.py", line 23, in vydel
    return a / b
ZeroDivisionError: division by zero

----------------------------------------------------------------------
Ran 5 tests in 0.005s

FAILED (errors=1)

Vidíme, že chyba je zachycena a jsme na ni upozorněni. Můžeme kód vrátit zpět do původního stavu.

V příští lekci, Testování v Pythonu - PyHamcrest a best practices, se podíváme na knihovnu PyHamcrest, vysvětlíme si očekávané chyby a zmíníme nejdůležitější best practices pro psaní testů.


 

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 166x (1.8 kB)

 

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)
Testování v Pythonu - PyHamcrest a best practices
Článek pro vás napsal Patrik Bernat
Avatar
Uživatelské hodnocení:
40 hlasů
Aktivity