Testování v Pythonu

Python je moderní skriptovací jazyk, který je stále populárnější i mezi webovými vývojáři. Za svou popularitu vděčí nejen svému návrhu a implementaci, ale také množství knihoven a nástrojů, které pro tento jazyk existují. V článku se seznámíme se základními možnostmi, které Python nabízí pro testování.

Žádný programátor není dokonalý a každý dělá chyby. Bývá proto dobrým programátorským zvykem svůj kód testovat – dělá to takřka každý. Veliký rozdíl ale je ve způsobu, jakým programátoři kód testují.

Nejpoužívanější přístup je asi metoda „zkusím to a uvidím“ – napíšu nějaký kód, spustím ho a pokud se dostaví očekávaný výsledek, považují kód za správný. V případě, že se jedná jen o část nějakého programu (funkce, třída, metoda, makro, …), často nezbývá, než se (obzvlášť v prostředí web aplikaci) uchýlit k ladícím výpisům a různým trikům, které nám dají informace o tom, jaký byl výsledek přímo toho kusu kódu, který chceme otestovat.

Testování tedy není „něco navíc“, co běžní programátoři nedělají, je to něco, co dělá každý – jen někteří (ti méně líní) to dělají ručně a opakovaně (dokud daný kód nefunguje). Skutečně líný programátor ale nejradši napíše test, aby tuto opakovanou práci za něj vykonával nějaký kus kódu – může to být (ale málokdy bývá) náročnější než otestovat danou věc ručně, ale je to něco, co autor kódu udělá jednou a pak to za něj vykonává automat – ve výsledku tedy výrazná časová úspora.

Navíc má autor kódu po napsání testu několik pěkných záruk – jelikož může kdykoli spustit dané testy a ověřit si, že je kód stále funkční, nemusí se tolik bát pustit se do změn v kódu, nemusí se bát ani svěřit kód jinému (i méně zkušenému) programátorovi a nehrozí mu, že změna v jedné části kódu rozbije něco úplně jinde, aniž by si toho někdo všiml.

Testování

Co tedy je test? V podstatě se jedná o zápis nějakého předpokladu (anglicky assertion od slovesa assert – předpokládat) do kódu. V Pythonu existuje přímo klíčové slovo assert, které dělá přesně to: kontroluje zda nějaký předpoklad platí a, pokud ne, vyhodí AssertionError výjimku. Zápis:

assert expression1, expression2

je ekvivalentní zápisu:

if __debug__:
    if not expression1: raise AssertionError(expression2)

kde __debug__ je interní proměnná Pythonu, která je vždy pravdivá, pokud nebyl Python interpret spuštěn s podporou optimalizací (-O).

Psaní testů ručně pomoci podmínek či assert výrazů je jedna z možnosti, je to ale zdlouhavé a zbytečně složité, obzvlášť když Python už přímo v základní knihovně obsahuje několik nástrojů, které psaní testu usnadní. Asi nejsnazší způsob psaní testu, se kterým se v Pythonu můžeme setkat, jsou doctesty.

Doctests

Doctest je v podstatě přepis session (dialogu) z interaktivního interpretru do dokumentačního řetězce modulu, třídy, metody nebo funkce. Asi nejlépe je to pochopitelné na příkladu:

#!/usr/bin/env python

"""
Make sure our python interpretter is sane
>>> 2+5
7
"""
if __name__ == '__main__':
    import doctest
    doctest.testmod()

Když se na tento příklad podíváme po částech, vidíme standardní hlavičku spustitelných skriptů (#!/usr/bin/env python), docstring k modulu, který obsahuje nás test, a krátký kus kódu, který zajistí, že se doctesty pustí, a to pouze v případě, že je daný skript puštěný přímo (buď jako spustitelný soubor nebo příkazem python test1.py) a nikoli když bude naimportovaný jako modul do jiného skriptu.

Výstup volání python test1.py by měl být prázdný a návratová hodnota by měla být 0. Při spuštění s parametrem -v 1 dostaneme podrobnější informace o tom, co se testuje a s jakým výsledkem:

# python test1.py -v 1
Trying:
    2+5
Expecting:
    7
ok
1 items passed all tests:
1 tests in __main__
1 tests in 1 items.
1 passed and 0 failed.
Test passed.

Doctesty v dokumentačních řetězcích lze libovolně kombinovat s psanou dokumentací (ta musí být od doctestu oddělena prázdným řádkem). To z nich činí výborný nástroj pro dokumentaci API, protože dávají uživateli jasnou představu, jak daný kód má fungovat. Fakt, že jsou současně spustitelné, nám zaručuje, že taková dokumentace bude velmi snadno udržovatelná v konzistentním stavu s kódem. Rozsáhlejší příklad z reálného světa by mohl vypadat nějak takto:

#!/usr/bin/env python

class SortedDict(dict):
    """
    Dictionary that maintains keys in sorted order (by time of definition).

    >>> d = SortedDict()
    >>> d['some-key'] = 1
    >>> d['some-key']
    1
    >>> d['another-key'] = 42
    >>> d[('tuples', 'can', 'be', 'keys', 'too')] = None
    >>> d.keys()
    ['some-key', 'another-key', ('tuples', 'can', 'be', 'keys', 'too')]
    """
    def __init__(self, *args, **kwargs):
        super(SortedDict, self).__init__(*args, **kwargs)
        self._key_order = []

    def __setitem__(self, key, value):
        if key not in self:
            self._key_order.append(key)
        super(SortedDict, self).__setitem__(key, value)

    def keys(self):
        return self._key_order

    # rest of implementation

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Jakkoli jsou doctesty jednoduché na psaní a výborné pro dokumentaci, jejich využití na větší testy nebývá doporučováno – doctesty se obecně špatně udržují a obtížné ladí. V našem příkladě, pokud bychom měli chybu v metodě __init__ která by zapříčinila selhání první řádky testu, bychom dostali ještě čtyři nic neříkající chyby o tom že proměnná d není definována a určitě by chvíli trvalo, než bychom našli ten relevantní řádek testu, na kterém je skutečná chyba, a tím kus kódu kde chyba skutečně je.

Další velká nevýhoda doctestu je, že se spoléhají na textovou reprezentaci, která musí být stejná – tedy test:

>>> s = u'Unicode string'
>>> s
'Unicode string'

vždy selže, jelikož se bude porovnávat přímo textová hodnota (s ‚u‘ na začátku), která nebude stejná. Obdobný assert by ale prošel zcela bez problémů:

assert u'Unicode string' == 'Unicode string'

Další problém s textovou reprezentací může být se slovníky, které nemají definováno pořadí klíčů. To se tak může lišit (a liší) mezi jednotlivými implementacemi Pythonu. To znamená, že test:

>>> {'a': 1, 'b': 42, 42: 27}
{'a': 1, 'b': 42, 42: 27}

může a nemusí projít (jedna z nejhorších možných vlastnosti pro test).

Jestliže potřebujeme dělat komplexnější testy, je lepší sáhnout po knihovně unittest, která je (stejně jako doctest) součástí základní knihovny pythonu, a není tak třeba nic instalovat.

Unit tests

Pythoní knihovna unittest je založena na známém nástroji JUnit pro Javu, který je založen na obdobné knihovně ve Smalltalku. Unit test pro nás v tomto článku bude znamenat test využívající knihovnu unittest, což nemusí (a často ani nebývá) skutečný jednotkový test.

Základním kamenem našich unit testů bude třída TestSortedDict, která dědí z unittest.Tes­tCase. Třída unittest.TestCase obsahuje spoustu užitečných metod, které můžeme v našich testech použít, a zároveň máme zaručeno, že standardní nástroje pro spouštění testů takový test najdou.

Podobně jako doctest, i unittest má jednoduchý způsob, jak spustit testy – unittest.main() – který najde všechny potomky unittest.TestCase a provede všechny testy v nich.

Nejdůležitějšími metodami pro nás jsou:

setUp()
Pouští se před spuštěním každého jednotlivého testu (tedy každé testovací metody, nikoliv jen před spuštěním test casu). Ideální místo pro různé inicializace a definice.
tearDown()
Pouští se po každém proběhlém testu (bez ohledu na výsledek) a má na starosti úklid prostředí – smazání dočasných souborů, vrácení DB do původního stavu atd.
assert_(expr[, msg])
ekvivalent built-in výrazu assert – selže (označí test za selhaný) pokud expr nebude mít hodnotu True. Pokud bude předán i parametr msg, použije se jako důvod selhání testu, jinak se použije None.
assertEqual(fir­st, second[, msg])
selže pokud first != second
assertRaisese(ex­ception, callable, *args, kwargs)
selže pokud volání callable(*args, kwargs) neskončí výjimkou exception

Za test se považuje každá metoda, jejíž jméno začíná na test_. Při psaní unit testů je dobré dodržovat několik pravidel:

  • pojmenovávat testy co nejpopisněji – když test spadne a já to uvidím ve výpisu, mělo by mi být ihned jasné, co daný test má testovat, a to nikoli na úrovni kódu (test_two_plus_f­our_equals_six nebo test_is_square_re­turns_falše_if_x_is_l­onger_than_y), ale na úrovni funkcionality (test_adding_wor­ks či test_rectangle_is_not_a­_square).
  • v každém testu testovat jen jednu věc – někdo zastává názor, že každý test by měl obsahovat právě jeden assert, dle mého názoru je to často přehnané, na druhou stranu rozdělovat testy a skutečně v každém jednotlivém testu testovat co nejméně vlastností považuji za velmi důležité – snáze díky tomu lokalizujeme případný problém, protože každý padající test se bude týkat jen malé části funkcionality.

Náš kód z předchozího příkladu bude tedy vypadat nějak takto:

#!/usr/bin/env python

import unittest

class SortedDict(dict):
    """
    Dictionary that maintains keys in sorted order (by time of definition).
    """
    def __init__(self, *args, **kwargs):
        super(SortedDict, self).__init__(*args, **kwargs)
        self._key_order = []

    def __setitem__(self, key, value):
        if key not in self:
            self._key_order.append(key)
        super(SortedDict, self).__setitem__(key, value)

    def keys(self):
        return self._key_order

    # rest of implementation

class TestSortedDict(unittest.TestCase):
    def setUp(self):
        self.d = SortedDict()

    def test_inserted_item_can_be_accessed(self):
        self.d['some-key'] = 1
        self.assertEqual(1, self.d['some-key'])

    def test_keys_maintains_order(self):
        self.d['some-key'] = 1
        self.d['another-key'] = 42
        self.d[('tuples', 'can', 'be', 'keys', 'too')] = None
        self.assertEqual(
            ['some-key', 'another-key', ('tuples', 'can', 'be', 'keys', 'too')],
            self.d.keys()
        )


if __name__ == '__main__':
    unittest.main()

Testy opět spustíme příkazem python test3.py, měli bychom vidět takovýto výstup:

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Za každý test máme ve výstupu tečku pokud prošel, ‚F‘ pokud neprošel a ‚E‘ pokud nastala chyba (když neplatí předpoklad v assert výrazu, jedna se o neprocházející test, pokud se někde něco pokazí jiného – vyhozena výjimka například – jedná se o chybu).

Unittest je poměrně základní knihovna, která obsahuje jen funkcionalitu absolutně nutnou pro psaní testů. Už nějakou dobu se nevyvíjela, a to zapříčinilo vznik mnoha testovacích knihoven a frameworků. V pythonu 2.7 a 3.2 se objevuje nová knihovna unittest2, která obsahuje nejlepší nápady a přístupy, použité v těchto knihovnách. Knihovna je k dispozici i jako samostatný balík, který lze doinstalovat i do starších verzi pythonu (od verze 2.4).

Spouštění testů

Ukázali jsme si dva možné zápisy testu v Pythonu i s možnostmi jak dané testy pustit, vždy se ale jednalo jen o jeden soubor. To může stačit pro krátké skriptíky, které se i s testy do jednoho souboru pohodlně vejdou, ale už ne pro střední a větší projekty, u kterých je potřeba testy spouštět pro více souborů (které by navíc měly být umístěné spolu, někde mimo hlavní kód).

Toto je jedna z věcí, kterou knihovna unittest2, na rozdíl od starší verze, řeší – stačí nainstalovat unittest2 a discover, a pak nám bude stačit jeden příkaz na spuštění všech unit testů v nějakém adresáři (skript standardně hledá testy v souborech které se jmenují test*.py):

unit2 discover

Knihovna unittest2 obsahuje i spoustu jiných užitečných vylepšení, jako například možnost přeskočit test, či nové assert metody. Doporučený způsob používání knihovny je naimportovat ji pod jménem unittest kvůli dopředné kompatibilitě.

Co dál?

Ukázali jsme si několik způsobů, jak v Pythonu psát testy, spolu s hlavními důvody, proč něco takového dělat. Shrnuli jsme si, jaké jsou výhody a nevýhody jednotlivých přístupů. Zájemci naleznou další informace v dokumentaci balíku unittest, která je výborně popsána a obsahuje i odkazy na další externí zdroje. Solidní ukázku, jak testování použít, dává i kapitola Unit Testing knihy Dive Into Python.

Pokud vás téma zaujalo, budeme se v dalším článku věnovat některé konkrétní oblasti testů v Pythonu – v komentářích můžete napsat, jaká problematika vás zajímá.

Testujete svůj kód?

Donedávna byl SW architektem Centrum Holdings. Zaměřuje se na datové sklady a databáze a propaguje Python a Django.

Komentáře: 32

Přehled komentářů

jakub vysoky [kvbik] kdo chce mit tecky i pro doctesty
xx Re: Anketa
Aleš Roubíček Re: Anketa
Martin Malý Re: Anketa
xx Re: Anketa
Aleš Roubíček Re: Anketa
xx Re: Anketa
imploder Re: Anketa
xx Re: Anketa
imploder Re: Anketa
xx Re: Anketa
sg Re: Anketa
xx Re: Anketa
Honza Kral Re: Anketa
tiso Re: Anketa
Martin Malý Re: Anketa
tiso Re: Anketa
Karel Minařík Re: Anketa
Jiří Knesl Re: Anketa
Kita Re: Anketa
msgre Coverage
Almad Re: Coverage
msgre Re: Coverage
Almad Re: Coverage
Honza Kral Re: Coverage
msgre Re: Coverage
xx Cenzura na Zdrojáku?
Martin Malý Re: Cenzura na Zdrojáku?
xx Re: Cenzura na Zdrojáku?
Michal Hořejšek assertRaises
lzap Pěkné
myneur O čem by mohl být další článek
Zdroj: https://www.zdrojak.cz/?p=3214