Testování v Pythonu

Testování softwaru je komplexní téma, ale je důležité se zaměřit na jeho hlavní výhody. Proč by měli vývojáři psát testy, když by mohli raději pracovat na nových funkcích nebo vylepšování aplikace? Jednoduše proto, že testy mohou zachránit aplikaci a vývojáře před tím, aby chyba byla zjištěna uživateli. Testy pomáhají zajistit, že aplikace bude fungovat správně, zabraňují tomu, aby aplikace nějakou dobu nefungovala a předcházejí nekonzistenci dat. Psaní testů může být sice zdržením, ale může ušetřit spoustu času a peněz v dlouhodobém horizontu.

Nejčastější typy testů

  • Jednotkové testy se zaměřují na ověření správnosti chování malých částí kódu. Tyto části kódu, jednotky, mohou být například funkce, metody nebo třídy. Zjednodušeně testujeme, že funkce vydává při různých vstupních parametrech správný výstup. Jako argumenty testovaných funkcí volíme schválně i krajní use-casy, které nemohou při běžném používání nastat tak často.
  • Integrační testy se zaměřují na ověření toho, zda jednotky kódu spolupracují správně a propojují se navzájem tak, jak by měly. Cílem integračních testů je tedy ověřit správnost interakcí mezi různými částmi softwaru, jako jsou například moduly, služby, systémy nebo databáze. Představme si například funkci create_article(...), která by měla do databáze uložit nový článek. V takové funkci často najdeme volání různých menších funkcí pro validaci jednotlivých vstupů a samotné volání databáze. Cílem integračního testu by mělo být zavolání funkce create_article(...), nezávislé načtení dat z databáze a porovnání, že v databázi se nachází opravdu to, co by tam mělo být.
  • End-to-end (E2E) testy se soustředí na ověření funkčnosti softwaru jako celku z pohledu uživatele. Tento typ testování simuluje reálné scénáře použití a ověřuje, zda aplikace funguje tak, jak by měla, od uživatelského rozhraní až po datové vrstvy. Patří zde například Selenium testy, které jsou schopny otevřít webový prohlížeč, automatizovaně vyplnit webový formulář, kliknout na tlačítko "uložit" a zkontrolovat, že takto vytvořený článek se skutečně zobrazil v seznamu nejnovějších článků. Když se budeme bavit o REST API, můžeme zde zařadit například testování json schémat, při kterém uděláme request na konkrétní endpoint a testujeme, že se nám vrátila správná data (ve stromové struktuře dat jsou klíče, které tam být mají, hodnoty jsou správného typu apod.).

Automatizované testování

Testy jsou neocenitelným nástrojem pro odhalování chyb v kódu aplikace. Aby byly co nejefektivnější, musí být spouštěny pravidelně a automaticky. I když si testy lze představit jako skripty, které se pustí jednoduše z příkazové řádky, málokterý vývojář by je spouštěl manuálně po každé své změně. Proto je důležité zajistit, aby testy byly spouštěny automaticky při každé relevantní akci, například při nahrání kódu do git repozitáře. Tento proces se nejčastěji provádí pomocí CI/CD nástrojů. Díky tomu jsou testy spouštěny pravidelně, včas a dokážou odhalit chyby dříve, než se dostanou do produkčního prostředí, což může být klíčové pro zachování kvality aplikace.

Slovníček pojmů

  • CI/CD - zkratka pro Continuous Integration/Continuous Delivery a představuje metodiku, která se používá při vývoji softwaru. CI/CD znamená, že při každé změně v kódu se automaticky spouštějí testy a integrační procesy, aby se ověřilo, že nové změny neporušily funkčnost aplikace a nepřinesly žádné nové chyby. Typickým prostředím pro CI/CD může být GitlabCI nebo Jenkins. V tomto prostředí se například po git push spustí tzv. pipeline složená z jobů, které si můžeme představit jako bash skripty. Automaticky se tak povýší verze aplikace, ubalí se nový docker image nebo se spustí právě testy.
  • mock - technika, při které vytváříme náhražková data a funkce, která simulují reálná data a funkce, ale jsou speciálně navržena tak, aby byla předvídatelná a opakovatelná pro účely testování. Například můžeme vytvořit mock data, která jsou podobná těm, která se nacházejí v produkční databázi, ale s jistotou, že mají vždy stejný počáteční stav při spuštění testů a dobře pokrývají krajní situace. Stejně tak můžeme použít mockování pro funkce nebo služby třetích stran. To znamená, že nebudeme spoléhat na skutečnou komunikaci s těmito službami, ale vytvoříme simulaci této komunikace a budeme vracet požadovanou odpověď podle testovaného scénáře. Tento přístup zabrání tomu, aby naše testy selhaly ve chvíli, kdy bude služba třetí strany nedostupná.
  • fixture - sada dat a nastavení, které definují počáteční stav testovacího prostředí a chování, které se očekává v průběhu testů. Fixture může obsahovat například konfiguraci aplikace, předem definovaná data nebo inicializaci testovacích objektů. Příkladem může být vytvoření testovacího klienta REST API, nad kterým v rámci integračních testů budeme posílat requesty.

Testovací frameworky v Pythonu

Frameworků pro psaní testů existuje v Pythonu celá škála. Nejčastěji se však setkáte s těmito dvěma: unittest, pytest.

unittest

Základní framework pro psaní testů v Pythonu, který je součástí standardní knihovny. Podporuje vytváření testovacích tříd, testovacích metod a funkcí pro kontrolu očekávaných výsledků.

import unittest

# Funkce, kterou chceme testovat
def add_numbers(a, b):
    return a + b

# Testovací třída pro funkci add_numbers
class TestAddNumbers(unittest.TestCase):

    # Testovací metoda pro ověření, zda funkce add_numbers správně sečte dvě čísla
    def test_add_numbers(self):
        result = add_numbers(2, 3)
        self.assertEqual(result, 5)

V této ukázce nejprve definujeme funkci add_numbers, kterou chceme testovat. Poté definujeme testovací třídu TestAddNumbers, která dědí z třídy unittest.TestCase. Tato třída poskytuje metody pro testování, jako jsou například assertEqual, assertTrue atd. které zachycují očekávaný výsledek.

V naší testovací třídě TestAddNumbers jsme napsali jednu testovací metodu test_add_numbers, která volá funkci add_numbers s argumenty 2 a 3 a ověřuje, zda vrácená hodnota je správná pomocí metody assertEqual. Testovací funkce by měly začínat prefixem test_, stejně tak testované třídy by měly dědit z třídy TestCase.

Spuštění testů pak probíhá buď pomocí zavolání unittest.main() nebo spuštění přes příkazový řádek:

python -m unittest

pytest

Pytest je volně dostupný open source framework, který si můžete nainstalovat z oficiálního repozitáře balíčků PyPI. Používá jednoduchou a intuitivní syntaxi, která zjednodušuje psaní testů a umožňuje jejich rychlejší vývoj. Poskytuje několik dekorátorů, které umožňují definovat chování testů a vývojáři ušetří nějaké to psaní. Patří mezi ně například oblíbený @pytest.mark.parametrize, který slouží k definování parametrů, které se mají použít při spouštění testu. Pomocí tohoto dekorátoru lze vytvořit mnoho variant stejného testu, přičemž každá varianta používá jiné parametry. Pytest umožňuje spouštět testy paralelně na více vláknech nebo procesech. To může výrazně zrychlit testování, zejména v případech, kdy jsou testy časově náročné nebo kdy se testuje velké množství dat.

def multiply_numbers(x, y):
    return x * y

class TestMultiply:
    def test_multiply_numbers(self):
        assert multiply_numbers(2, 3) == 6
        assert multiply_numbers(0, 5) == 0
        assert multiply_numbers(-2, 3) == -6

Syntaxe zápisu testu je podobná tomu ve frameworku unittest, všimněte si však zjednodušení v podobě klíčového slova assert při porovnání získaného a očekávaného výsledku. Abychom zajistili, že pytest najde v adresáři rekurzivně testy sám, je ve výchozím nastavení nutné, aby názvy souborů s testy a testovací funkce začínaly prefixem "test_", třídy pak prefixem "Test". Tyto prefixy není nutné dodržet, pokud pytestu řekneme přesně, jaký test má spustit, ale jedná se o konvenci, kterou doporučuji dodržet. Pokud máme jednodušší funkcionalitu, můžeme vynechat obalení "Test" třídou.

Detailně si zkusíme tyto dva frameworky rozebrat v dalších článcích.

Úroveň znalostí
Začátečník