Přejít k navigační liště

Zdroják » Různé » Testování v Pythonu

Testování v Pythonu

Články Různé

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?

Komentáře

Subscribe
Upozornit na
guest
32 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
jakub vysoky [kvbik]

chvalim pouzivani unittest2, i kdyz te knihovne urcite jeste nejakou chvili potrva, nez nahradi napriklad nose (a i kdyz jejich puvodni zamereni mohlo byt malinko jine).

u testovani se obecne mluvi o takzvane „zavislosti na teckach“ a pokud tedy nekdo chcete mit tecky take pro doctesty, nabizi puvodni knihovna unittest nasledujici moznost (a mozna i spoustu dalsich ;)):

if __name__ == '__main__':
    import unittest
    import doctest

    suite = unittest.TestSuite()
    suite.addTest(doctest.DocTestSuite())

    runner = unittest.TextTestRunner()
    runner.run(suite)

http://github.com/kvbik/bordel/blob/master/py/slova.py#L42

xx

Proč v anketě není volba „Ne, píšu korektní kód, takže testy nepotřebuji.“?

Aleš Roubíček

Protože nikdo takový neexistuje. ;)

Martin Malý

Protože jsem chtěl, aby možnosti v anketě byly alespoň trochu reálné.

xx

Co je na tom nereálného?

Aleš Roubíček

Nikdo není neomylný.

xx

Korektnost programu lze dokázat a důkazy může počítač automaticky ověřit.

imploder

Takže napíšu program třeba v Pythonu a počítač mi řekne, jestli dělá všechno jak má? A jak počítač ví, co po něm vlastně chci? On to neví, musí mu to někdo říct. A když ten někdo při tom udělá chybu, tak jsme zase tam, kde jsme byli.

Na automatické ověřování se hodí testy. V důkazu, co někdo vymyslí, může být chyba. A ne pro každý program jde formální důkaz udělat.

xx

On to neví, musí mu to někdo říct.

Jistě, že musíte mít formální specifikaci toho, co má program dělat. Bez toho se nemá smysl bavit o korektnosti programu.

Na automatické ověřování se hodí testy. V důkazu, co někdo vymyslí, může být chyba.

Jenže v testu může být také chyba.

A ne pro každý program jde formální důkaz udělat.

IMO ve specifikaci jsou vlastnosti, co lze dokázat.

imploder

Jenže v testu může být také chyba.

To může. Akorát test obvykle není složitější než samotný program.

Důkaz je skvělá věc – co víc si přát – ale případ „programy netestuju, všechno mám dokázané, tak testovat nepotřebuju“ je nereálný. Takový člověk by toho moc nenaprogramoval.

xx

Takový člověk by toho moc nenaprogramoval.

Ano, ale já se programováním neživím, takže mi to nevadí.

To může. Akorát test obvykle není složitější než samotný program.

Souhlasím.

Ale přesto by si měl každý uvědomit, že testy může pouze ukázat, že program nefunguje (s výjimkou konečných automatů, kde může dokázat i korektnost).

sg

Formalní přístupy jsou v dnešní době používané pouze ve specifických aplikacích (kritických aplikacích, tedy tam, kde sebelepší testsuite nestačí – ať ekonomicky nebo bezpečně). Navíc vedle nich se také používá klasické testování a statická analýza. Tento článek popisuje pouze testování, navíc pouze pro Python. Já osobně si nejsem vědom nějakého nástroje, který implementujte formální metody nad programy v Pythonu.

xx

Původně jsem reagoval na nabídku možností v anketě.

Já osobně si nejsem vědom nějakého nástroje, který implementujte formální metody nad programy v Pythonu.

To já také ne. Ale myslím si, že by nebylo příliš složité rozšířit třeba Coq, aby se daly vyextrahovat programy i v Pythonu.

Honza Kral

Nikdo nerika ze testy nemuzou byt ve forme dukazu :)

Jestli to pro vase problemy funguje a dokazete to tak nam ostatnim nezbyva nez vam zavidet. Cokoliv receno o automatickych testech to ale nerozporuje – vy jen testujete dukladneji pomoci dukazu.

tiso

Chýba mi tam možnosť „ne, zatím“ alebo len „ne“ bez toho dodatku o zdržovaní a zvyšovaní nákladov, s ktorým nesúhlasím.

Martin Malý

Pokud ne, tak by mě zajímalo, proč ne – snad nejčastější odpověď, se kterou jsem se setkal, byla právě ta o zdržování a zvyšování nákladů, proto jsem ji tam dal. Možná by stálo za to udělat anketu „proč netestujete?“ ;)

tiso

Áno, to sú tie najčastejšie výhovorky prečo netestovať. Ale s touto škatuľkou nechcem mať nič spoločné.
Prečo netestujem? Ešte som neprenikol do testovania tak hlboko (teória, výber správnych nástrojov, Best/Worst practices …), aby som týmto spôsobom pracoval.

Karel Minařík

Nejen anketu, nejlíp článek nebo sérií rozhovorů :) Já vždy na workshopech říkám, že _každý_, i ten kdo netestuje, umí vyjmenovat důvody, proč jsou automatizované testy výhodné. A právě u těch odpovědí proč _netestovat_ to začne být zajímavé.

Mimochodem, hlavní důvod je opravdu ten, že lidé nevědí _jak_ na to. Zmiňované „není čas, nejsou peníze, nejsou lidi“ začíná být tak trochu fáma (platí to jen u absolutně hibernovaných týmů) – právě proto, že většina lidí dobře chápe, že je velmi slabý argument…

Jiří Knesl

Protože apríl byl už skoro před 3 týdny.

Kita

Protože to není o korektnosti psani kódu ale o funčnosti.

Michal Valoušek

Honzo,
používáte v Centru při testování nějakou variantu Coverage?

Díky za odpověď

Almad

Ano.

Michal Valoušek

A kterou? :)

Tu od Neda Batcheldera, nebo máte zkušenosti i s jiným řešením?

Almad

Jeden čas jsme zkoušeli i Titusovu verzi, ale s novou verzí coverage neni důvod, Ned ji přece jenom udržuje víc ,)

Každopádně jedeme přes nose, takže jestli –with-coverage nebo –with-figleaf neni moc rozdíl :)

Navíc je taknějak funkční i branch coverage, ale s tou jsme si zatím moc nehráli.

Honza Kral

Jsem velky fanousek coverage ale ma to nekolik uskali: neni to spolehliva metrika na mereni kvality testu, jen ukazuje (podobne jako testy same) ze je neco spatne, ne ze je neco dobre.

Nejlepsi vyuziti coverage je pr trackovani trendu – zda se pokryti zvetsuje ci zmensuje – je to dulezite abychom vedeli zda novy kod ktery pridavame je otestovany nebo pridavame featury bez testu. V kazdem zdravem projektu by se mela coverage zvysovat nehlede na absolutni hodnotu. samozrejme idealni je mit tohle v CI nastroji (hudson, buildbot, …) ktery je k tomu schopen prilepit dalsi info (jako kdo nejvic snizuje ci zvysuje coverage projektu)

Michal Valoušek

Ze souvislostí mezi procenty pokrytí a kvalitou testů jsem už vyrostl… :)

Mě se Coverage osvědčil hlavně v případech, když si *myslím* že mám proklepnuté všechny důležité věci. Omyl! Vždycky se najde nějaké „nepokryté“ místo, na které jsem zapomněl (nejčastěji nějaká větev if podmínky).

PS: figleaf neznám, díky za tip!

xx

Proč byl komentář Daniela Steigerwalda, můj a uživatele yy odstraněn?

Martin Malý

Protože byly naprosto offtopic, netýkaly se ani tématu článku, ani tématu vlákna, ale existence diskusního tématu samotného. Na takové hovory tu je patřičné vlákno v diskusním fóru, kde lze diskutovat o „cenzuře“ a ztraceném času doalelujá, aniž by to rušilo ty, co si přišli přečíst článek a komentáře k tématu.

xx

Díky za odpověď.

Michal Hořejšek

V článku v části popisování Unittestů jsou popisovány metody, které lze použít a poslední zmiňovaná, assertRaisese, je napsána s překlepem – poslední e v názvu není. Název je assertRaises.

lzap

Hezké, ale článek by se měl jmenovat spíše Unit testy v Pythonu. Je celý věnovaný jen a pouze unit testům. Testování má mnoho podob a tváří.

JUnit je spíše inspirován Smalltalkem. Nevhodně zvolené slovo „založen“.

Moc pěkně zpracováno.

myneur

V diskusi tu zazněl podle mě trefný názor proč lidi netestují – neví jak na to. To by mohl být námět na další článek – ukázat testování v praxi na nějakém fragmentu komplexnějšího systému. Úvodů do testování a jiných testovacích hello worldů je po netu mnoho, ale jak pojmout testování jako celek, kde se objeví nějaké zapeklitosti, na co si dát pozor, jak to prostě vypadá u složitějších projektů. Samozřejmě se lze podívat do testů již existujících projektů třeba na Git Hubu, ale dobré vedení by určitě lidem velmi usnadnilo úvodní tápání a mohlo změnit přístup k testům.
Kdy už pythoní unittest nestačí a hodí sane nebo nose a jak to využít. A v nějakém dalším díle třeba až jak na continuous integration.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.