Obsah
1. Omezení stavového prostoru testovaných funkcí a metod
2. Příklad polymorfní funkce v Pythonu
3. Nástroj mypy aneb nepovinná deklarace datových typů
4. Funkce akceptující parametry vybraných typů
6. Návratová hodnota generické funkce
7. Postupná úprava projektu s funkcí pro výpočet faktoriálu
8. Rozšíření jednotkových testů pro pokrytí všech větví kódu
9. Chyby kvůli chybějícím typovým kontrolám
10. Explicitní typové kontroly prováděné v čase běhu aplikace
11. Použití typové deklarace a kontrola s využitím mypy
12. Pokus o použití typové deklarace používající Union
13. Využití deklarace Optional
14. Úprava těla funkce takovým způsobem, aby byla typově bezpečná
15. Korektní způsob – využití přetížení funkce
16. Jak pracné je přidat informace o typech do složitějších funkcí?
17. Specifikace typů kontejnerů
18. Repositář s demonstračními příklady
19. Předchozí články s tématem testování (nejenom) v Pythonu
1. Omezení stavového prostoru testovaných funkcí a metod
„A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in.“
John Carmack
V sedmém článku o tvorbě testů s využitím programovacího jazyka Python se budeme zabývat tématem, které sice může být považováno za okrajové, ovšem ve skutečnosti je pro psaní dobrých testů poměrně důležité. Jedná se o to, jakým způsobem vůbec může autor jednotkových testů (popř. později testů integračních) vůbec s rozumně velkým úsilím obsáhnout celý stavový prostor testovaných modulů, ať již se jedná o funkce, metody, třídy či o celé balíčky. Nejjednodušší (ovšem jen relativně) je z pohledu testování stavový prostor čistých funkcí, tj. funkcí, které produkují výstup čistě na základě svých parametrů; nemají tedy vnitřní paměť. Složitější je situace u uzávěrů či generátorů, které vnitřní paměť mají a ještě rozsáhlejší stavový prostor najedeme u (netřídních, nestatických) metod či při testování celých tříd, zejména těch, které umožňují měnit stav objektů (a to vůbec nemluvíme o kódu, který mění globální proměnné).
V případě jednotkových testů je celá situace vlastně velmi spravedlivá – autorem jednotkových testů i vlastního kódu bývá stejný vývojář, jenž se buď může snažit o refaktoring kódu do takového stavu, že bude snadno testovatelný, či naopak bude věnovat více času psaní jednotkových testů (samozřejmě za předpokladu, že je jeho cílem mít stabilní a relativně bezchybný produkt). Refaktoring může směřovat k vytváření čistých funkcí, neměnných hodnot (immutable), k omezení globálního stavu aplikace apod. (ostatně i z tohoto důvodu je podle mého názoru vhodné alespoň nějaký čas věnovat studiu funkcionálních jazyků). V případě jazyka Python lze stavový prostor omezit – a to mnohdy dosti podstatným způsobem – zavedením nepovinných typových deklarací, což je nosné téma dnešního článku. Vedle snahy o tvorbu programů takovým způsobem, aby byl jejich stavový prostor co nejvíce omezen, existují i nástroje, které se po dodání vhodných informací snaží o otestování všech z nějakého pohledu zajímavých či extrémních stavů. Takovým nástrojem je Hypothesis, kterým se budeme zabývat příště.
„Many programming languages support programming in both functional and imperative style but the syntax and facilities of a language are typically optimised for only one of these styles, and social factors like coding conventions and libraries often force the programmer towards one of the styles. Therefore, programming languages may be categorized into functional and imperative ones.“
Jak jsme si již řekli v předchozím odstavci, dnes se budeme zabývat především způsobem, jak do kódu v Pythonu zavést statické typové informace, což je pochopitelně velmi kontroverzní téma, protože jeden z důvodů velké obliby Pythonu je právě možnost elegantního zápisu programů bez toho, aby se musel vývojář soustředit na (v daném kontextu) nepodstatné detaily. Navíc může být díky dynamickému typovému systému mnoho funkcí polymorfních, což opět vede ke zjednodušení tvorby programů. Příkladem může být standardní polymorfní funkce len, která pracuje korektně pro všechny datové typy, kde má slovo „délka“ význam: řetězce, n-tice, seznamy, množiny, objekt range atd. Programátor si tedy nemusí pamatovat (jako v některých jiných jazycích), že například délku kolekce získá metodou .size(), délku řetězce naopak metodou .length() a velikost pole pro změnu přečte z atributu length. Ovšem polymorfní funkce/metody mají pochopitelně i některé nepříjemné vlastnosti a ty se týkají testování.
2. Příklad polymorfní funkce v Pythonu
„Write unit tests.“
„Write unit tests.“
„Write unit tests.“
https://youtu.be/MYucYon2-lk?t=167
Podívejme se nyní na příklad běžné funkce, která však má v Pythonu polymorfní chování, konkrétně se chová různě pro různé typy hodnot předaných této funkci. Funkce je to vlastně velmi primitivní – porovná hodnoty svých dvou argumentů a vrátí hodnotu True pouze v případě, že je hodnota prvního argumentu „menší“, než hodnota argumentu druhého. Ovšem slovo „menší“ je nutné chápat v kontextu daného datového typu:
def compare(x, y): return x < y
Pro takovou funkci je možné napsat jednotkový test a přitom zcela ignorovat fakt, že porovnávat lze i jiné hodnoty, než celá čísla:
"""Jednotkové testy pro funkci compare.""" from comparator1 import compare def test_comparator(): """Jednotkový test pro porovnání dvou prvků.""" assert compare(1, 2) assert not compare(2, 1)
Tento jednotkový test proběhne v pořádku:
$ pytest -v test_comparator.py
S výsledky:
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/mypy plugins: print-0.1.3, voluptuous-1.0.2, cov-2.5.1 collecting ... collected 1 item comparator_test.py::test_comparator PASSED [100%] ============================== 1 passed in 0.01s ===============================
Podobně dopadneme i při zjišťování pokrytí kódu jednotkovými testy:
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 rootdir: /home/ptisnovs/src/python/testing-in-python/mypy plugins: print-0.1.3, voluptuous-1.0.2, cov-2.5.1 collected 1 item comparator_test.py . [100%] ============================== 1 passed in 0.02s ===============================
Stoprocentní pokrytí by mohlo vzbuzovat dojem, že se nám podařilo otestovat celé a úplné chování funkce – celý stavový prostor tvořený hodnotami jejich argumentů. Ten je v Pythonu skutečně obrovský, jak je naznačeno na následujícím diagramu:
┌──────────────────────────────────────────────────────────────────────........┐ │┌─────────────────────────┐┌────────────┐ ┌────────┐┌────────┐ │ ││ float ││ integer │ │ string ││ bytes │ tuple │ ││ ││ │ │ ││ │ │ ││ ││ ┌────┤┌──────┐ │ ││ │ list │ ││ ││ │bool││ None │ │ ││ │ │ │└─────────────────────────┘└───────┴────┘└──────┘ │ ││ │ dict │ │┌───────────────────────────────────────────┐ │ ││ │ │ ││ complex │ │ ││ │ ... │ ││ │ │ ││ │ ... │ ││ │ │ ││ │ ... │ ││ │ │ ││ │ │ │└───────────────────────────────────────────┘ └────────┘└────────┘ │ └──────────────────────────────────────────────────────────────────────........┘
Ve skutečnosti je však stavový prostor mnohem větší, protože Python umožňuje, aby se do funkce compare předaly i instance libovolné třídy:
┌───────────────────────────────────────────────────────────────────────────────────┐ │ │ │ ┌──────────────────────────────────────────────────────────────────────........┐ │ │ │┌─────────────────────────┐┌────────────┐ ┌────────┐┌────────┐ │ │ │ ││ float ││ integer │ │ string ││ bytes │ tuple │ │ │ ││ ││ │ │ ││ │ │ │ │ ││ ││ ┌────┤┌──────┐ │ ││ │ list │ │ │ ││ ││ │bool││ None │ │ ││ │ │ │ │ │└─────────────────────────┘└───────┴────┘└──────┘ │ ││ │ dict │ │ │ │┌───────────────────────────────────────────┐ │ ││ │ │ │ │ ││ complex │ │ ││ │ ... │ │ │ ││ │ │ ││ │ ... │ │ │ ││ │ │ ││ │ ... │ │ │ ││ │ │ ││ │ │ │ │ │└───────────────────────────────────────────┘ └────────┘└────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────........┘ │ │ │ │ │ │ všechny ostatní třídy │ │ │ │ │ └───────────────────────────────────────────────────────────────────────────────────┘
Můžeme se tedy pokusit napsat poněkud složitější jednotkový test:
"""Jednotkové testy pro funkci compare.""" from comparator1 import compare def test_comparator(): """Jednotkový test pro porovnání dvou prvků.""" assert compare(1, 2) assert compare(1.2, 3.4) assert not compare("foo", "bar") assert compare([1, 2], [3, 4]) assert not compare(True, False)
I tak jsme ovšem s celého stavového prostoru otestovali nepatrný zlomek všech možných kombinací:
┌──────────────────────────────────────────────────────────────────────────────────────────┐ │ │ │ ┌────────────────────────────────────────────────────────────────────────.............┐ │ │ │┌─────────────────────────┐┌─────────────┐ ┌─────────┐┌────────┐ │ │ │ ││ float ││ integer │ │ string ││ bytes │ tuple │ │ │ ││ ││[1,2] │ │ ││ │ │ │ │ ││ [1.2, 3.4] ││ ┌─────┤┌──────┐ │ ││ │ list │ │ │ ││ ││ │[T,F]││ None │ │ ││ │ [[1,2][3,4]]│ │ │ │└─────────────────────────┘└───────┴─────┘└──────┘ │ ││ │ │ │ │ │┌────────────────────────────────────────────┐ │ ││ │ dict │ │ │ ││ complex │ │[foo,bar]││ │ │ │ │ ││ │ │ ││ │ ... │ │ │ ││ │ │ ││ │ ... │ │ │ ││ │ │ ││ │ ... │ │ │ │└────────────────────────────────────────────┘ └─────────┘└────────┘ │ │ │ └────────────────────────────────────────────────────────────────────────.............┘ │ │ │ │ │ │ všechny ostatní třídy │ │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────┘
To mj. znamená, že ani stoprocentní pokrytí kódu testy nemusí vůbec nic znamenat a naše funkce nejenom že není řádně otestovaná, ale pro mnohé vstupy ani nepracuje korektně (resp. její chování není komutativní, což je od porovnání většinou očekáváno). Ostatně si to můžete ověřit:
"""Jednotkové testy pro funkci compare.""" from comparator1 import compare from math import nan def test_comparator(): """Jednotkový test komutativity.""" assert compare(1, 2) is not compare(2, 1) assert compare("foo", "bar") is not compare("bar", "foo") assert compare(nan, nan) is not compare(nan, nan)
S výsledky:
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/mypy plugins: print-0.1.3, voluptuous-1.0.2, cov-2.5.1 collecting ... collected 1 item comparator_test2.py::test_comparator FAILED [100%] =================================== FAILURES =================================== _______________________________ test_comparator ________________________________ def test_comparator(): """Jednotkový test komutativity.""" assert compare(1, 2) is not compare(2, 1) assert compare("foo", "bar") is not compare("bar", "foo") > assert compare(nan, nan) is not compare(nan, nan) E assert False is not False E + where False = compare(nan, nan) E + and False = compare(nan, nan) comparator_test2.py:11: AssertionError =========================== short test summary info ============================ FAILED comparator_test2.py::test_comparator - assert False is not False ============================== 1 failed in 0.03s ===============================
3. Nástroj mypy aneb nepovinná deklarace datových typů
„Types will save us!“
Již od doby vzniku prvních programovacích jazyků s dynamickým typovým systémem (LISP 1) existuje tenze mezi zastánci silného typového systému a dynamických typových systémů. Obě paradigmata mají své nesporné výhody, ale i zápory, což v důsledcích vedlo k řešení, které vidíme i dnes – samotný operační systém a jeho další součástí v user space jsou psány v jazycích se statickým (schválně nepíšu silným) typovým systémem, zatímco skripty jsou vytvářeny v dynamicky typovaných programovacích jazycích. Existují samozřejmě snahy o propojení obou světů a jedním z příkladů této snahy je i nástroj mypy, jehož některé vlastnosti si ukážeme v následujících kapitolách.
Mypy lze nainstalovat stejným způsobem, jako jakýkoli jiný balíček pro Python, tj. například s využitím nástroje pip/pip3:
$ pip3 install --user mypy
Ovšem vzhledem k poměrně rychlému vývoji tohoto nástroje může být lepší používat verzi získanou a nainstalovanou přímo z repositáře:
$ git clone --recurse-submodules https://github.com/python/mypy.git $ cd mypy $ sudo python3 -m pip install --upgrade .
Takto nainstalovaná verze mypy by se měla ohlásit následujícím způsobem:
$ mypy --version mypy 0.770+dev.1d7a6ae9fa313a93293a48585dcfc4e9dc482467
Existuje velké množství přepínačů a konfiguračních voleb. Prakticky žádnou z nich prozatím nebudeme potřebovat, ale pochopitelně je možné si je nechat vypsat ve formě nápovědy:
$ mypy --help usage: mypy [-h] [-v] [-V] [more options; see below] [-m MODULE] [-p PACKAGE] [-c PROGRAM_TEXT] [files ...] Mypy is a program that will type check your Python code. Pass in any files or folders you want to type check. Mypy will recursively traverse any provided folders to find .py files: $ mypy my_program.py my_src_folder For more information on getting started, see: ... ... ...
Podívejme se nyní na příklad použití tohoto nástroje. Nejprve si necháme zanalyzovat zdrojový kód s funkcí, která žádné informace o typech neobsahuje:
def compare(x, y): return x < y if __name__ == '__main__': # pouze se ujistíme, že lze spustit funkci compare print(compare(1, 2)) print(compare(1.2, 3.4)) print(compare("foo", "bar")) print(compare([1, 2], [3, 4])) print(compare(True, False))
Analýza proběhne bez chyby, a to z toho důvodu, že mypy nedokáže otestovat funkce a metody bez uvedení typových deklarací:
$ mypy comparator1.py Success: no issues found in 1 source file
Nyní do kódu přidáme informace o typech, které byly do Pythonu zavedeny v souvislosti s PEP-484. Specifikujeme, že parametry funkce by měly být typu celé číslo a výsledek pak pravdivostní hodnota:
def compare(x: int, y: int) -> bool: return x < y print(compare(1, 2)) print(compare(1.2, 3.4)) print(compare("foo", "bar")) print(compare([1, 2], [3, 4])) print(compare(True, False)) print("string" + compare(True, False))
Nyní již mypy dokáže detekovat problémy při volání funkce (a popř. při zpracování její návratové hodnoty):
$ mypy comparator2.py comparator2.py:6: error: Argument 1 to "compare" has incompatible type "float"; expected "int" comparator2.py:6: error: Argument 2 to "compare" has incompatible type "float"; expected "int" comparator2.py:7: error: Argument 1 to "compare" has incompatible type "str"; expected "int" comparator2.py:7: error: Argument 2 to "compare" has incompatible type "str"; expected "int" comparator2.py:8: error: Argument 1 to "compare" has incompatible type "List[int]"; expected "int" comparator2.py:8: error: Argument 2 to "compare" has incompatible type "List[int]"; expected "int" comparator2.py:11: error: Unsupported operand types for + ("str" and "bool") Found 7 errors in 1 file (checked 1 source file)
4. Funkce akceptující parametry vybraných typů
„These bugs are as instructive as they were devastating: They were rooted in the same programmer optimism, overconfidence, and haste that strike projects of all sizes and domains.“
Mike Bland
V některých situacích budeme potřebovat zajistit, aby nějaká funkce akceptovala parametry nejenom jednoho datového typu, ale vybraných typů. Poněkud umělým příkladem může být naše testovací funkce compare, jejíž typovou definici lze upravit takovým způsobem, aby akceptovala buď celá čísla, nebo i řetězce. K tomuto účelu se používá tato typová deklarace:
Union[typ1, typ2, ...]
Zápis je tedy možné upravit následujícím způsobem:
from typing import Union def compare(x: Union[int, str], y: Union[int, str]) -> bool: return x < y print(compare(1, 2)) print(compare(1.2, 3.4)) print(compare("foo", "bar")) print(compare([1, 2], [3, 4])) print(compare(True, False)) # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! print(compare(1, "bar")) print(compare("foo", 2))
Ve skutečnosti se ovšem problém s datovými typy nevyřešil uspokojivě, a to z toho důvodu, že jeden z operandů nyní může být typu int a druhý typu str či naopak (nikde není řečeno, že oba operandy musí být stejného datového typu). Na tuto skutečnost nás mypy upozorní, a to na jediném; konkrétně v samotném těle funkce, kde z možných čtyř kombinací jsou pouze dvě korektní:
comparator3.py:5: error: Unsupported operand types for < ("int" and "str") comparator3.py:5: error: Unsupported operand types for < ("str" and "int")
Naproti tomu samotné volání funkce, a to včetně dvou volání oddělených vykřičníky, je již z pohledu mypy korektní, protože typy přesně odpovídají typům deklarovaným:
$ mypy comparator3.py comparator3.py:5: note: Both left and right operands are unions comparator3.py:9: error: Argument 1 to "compare" has incompatible type "float"; expected "Union[int, str]" comparator3.py:9: error: Argument 2 to "compare" has incompatible type "float"; expected "Union[int, str]" comparator3.py:11: error: Argument 1 to "compare" has incompatible type "List[int]"; expected "Union[int, str]" comparator3.py:11: error: Argument 2 to "compare" has incompatible type "List[int]"; expected "Union[int, str]" Found 6 errors in 1 file (checked 1 source file)
5. Generické funkce
Nástroj mypy podporuje i deklaraci generických funkcí, resp. přesněji řečeno umožňuje zápis, který se do značné míry generickým funkcím podobá. Nejprve je nutné deklarovat nový datový typ, v našem případě odpovídající buď celým číslům nebo řetězcům:
IntOrString = TypeVar("IntOrString", str, int)
Tento typ, který se „naplní“ až u deklarace konkrétní funkce či jiného datového typu, se používá následujícím způsobem:
def compare(x: IntOrString, y: IntOrString) -> bool: ... ... ...
V této chvíli již můžeme náš demonstrační příklad s deklarací funkce compare dokončit:
from typing import Union from typing import TypeVar IntOrString = TypeVar("IntOrString", str, int) def compare(x: IntOrString, y: IntOrString) -> bool: return x < y print(compare(1, 2)) print(compare(1.2, 3.4)) print(compare("foo", "bar")) print(compare([1, 2], [3, 4])) print(compare(True, False))
A následně si nechat otestovat, zda a kdy je funkce použita správně:
$ mypy comparator4.py comparator4.py:12: error: Value of type variable "IntOrString" of "compare" cannot be "float" comparator4.py:14: error: Value of type variable "IntOrString" of "compare" cannot be "List[int]" Found 2 errors in 1 file (checked 1 source file)
6. Návratová hodnota generické funkce
Ukažme si ještě jeden příklad generické funkce. Tentokrát se bude jednat o funkci nazvanou add, která je určena buď pro součet dvou celých čísel nebo pro spojení dvou řetězců (pochopitelně se opět jedná o umělý „školní“ příklad). U této funkce určíme jak typ obou parametrů, tak i typ výsledné hodnoty – ve všech případech se musí jednat o shodný typ (buď číslo nebo řetězec):
from typing import TypeVar IntOrString = TypeVar("IntOrString", str, int) def add(x: IntOrString, y: IntOrString) -> IntOrString: return x + y print(add(1, 2)) print(add("foo", "bar")) print(add("foo", 2)) print(add(1, "bar")) print(42 + add(1,2)) print(42 + add("foo", "bar")) print("result: " + add("foo", "bar")) print("result: " + add(1, 2))
S chybami, které byly nalezeny nástrojem mypy:
adder.py:13: error: Value of type variable "IntOrString" of "add" cannot be "object" adder.py:14: error: Value of type variable "IntOrString" of "add" cannot be "object" adder.py:17: error: Unsupported operand types for + ("int" and "str") adder.py:20: error: Unsupported operand types for + ("str" and "int") Found 4 errors in 1 file (checked 1 source file)
7. Postupná úprava projektu s funkcí pro výpočet faktoriálu
V následujících kapitolách si ukážeme postupnou úpravu jednoduchého projektu s jedinou funkcí určenou pro rekurzivní výpočet faktoriálu. Opět se jedná o typický školní příklad, protože v praxi lze výpočet realizovat i efektivnější formou:
"""Výpočet faktoriálu.""" def factorial(n): """Rekurzivní výpočet faktoriálu.""" if n < 0: return None if n == 0: return 1 return n * factorial(n-1)
V jednotkových testech zjistíme, jak výpočet probíhá jak pro počáteční dvě vstupní hodnoty 0 a 1, tak i pro hodnotu, u níž muselo dojít k několika rekurzivním zanořením:
"""Jednotkové testy pro výpočet faktoriálu.""" from factorial import factorial def test_factorial(): """Jednotkový test pro výpočet faktoriálu.""" assert factorial(0) == 1 assert factorial(1) == 1 assert factorial(3) == 6
Výsledek běhu jednotkových testů ukazuje, že by mělo být vše v pořádku (což ovšem samozřejmě není pravda):
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/mypy/factorial1 plugins: print-0.1.3, voluptuous-1.0.2, cov-2.5.1 collecting ... collected 1 item factorial_test.py::test_factorial PASSED [100%] ============================== 1 passed in 0.01s ===============================
Detekce pokrytí kódu jednotkovými testy naznačuje, že jedna větev v podmínce vůbec není otestovaná, takže se nedosáhlo 100%:
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 rootdir: /home/ptisnovs/src/python/testing-in-python/mypy/factorial1 plugins: print-0.1.3, voluptuous-1.0.2, cov-2.5.1 collected 1 item factorial_test.py . [100%] ----------- coverage: platform linux, python 3.6.6-final-0 ----------- Name Stmts Miss Cover ---------------------------------- factorial.py 6 1 83% ============================== 1 passed in 0.03s ===============================
8. Rozšíření jednotkových testů pro pokrytí všech větví kódu
Samozřejmě nám nic nebrání se podívat na to, které řádky nejsou jednotkovými testy pokryty. Jedná se o první větev, která testuje, zda nejsou ve výpočtu použity záporné hodnoty:
"""Výpočet faktoriálu.""" def factorial(n): """Rekurzivní výpočet faktoriálu.""" if n < 0: return None if n == 0: return 1 return n * factorial(n-1)
Jednotkové testy lze rozšířit i o tyto hodnoty s otestováním, jaký výsledek dostaneme (faktoriál pro záporná čísla definován není, takže lze buď vyhodit výjimku či vrátit nějakou speciální hodnotu, což je i náš případ):
"""Jednotkové testy pro výpočet faktoriálu.""" from factorial import factorial def test_factorial(): """Jednotkový test pro výpočet faktoriálu.""" assert factorial(0) == 1 assert factorial(1) == 1 assert factorial(3) == 6 def test_factorial_negative_values(): """Jednotkový test pro výpočet faktoriálu.""" assert factorial(-1) is None assert factorial(-1000) is None
Nyní je již pokrytí kódu jednotkovými testy stoprocentní:
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 rootdir: /home/ptisnovs/src/python/testing-in-python/mypy/factorial2 plugins: print-0.1.3, voluptuous-1.0.2, cov-2.5.1 collected 2 items factorial_test.py .. [100%] ----------- coverage: platform linux, python 3.6.6-final-0 ----------- Name Stmts Miss Cover ---------------------------------- factorial.py 6 0 100% ============================== 2 passed in 0.03s ===============================
9. Chyby kvůli chybějícím typovým kontrolám
I přesto, že je pokrytí jednotkovými testy stoprocentní, není výpočet faktoriálu naprogramován bezchybně. Ostatně si to můžeme sami ověřit, například pokusem o předání parametru 2.1 (nebo jakékoli kladné hodnoty s nenulovou částí za desetinnou čárkou):
print(factorial(2.1))
V tomto případě dojde při rekurzi k tomu, že se výpočet rekurzivně zavolá se zápornou hodnotou (-0.9), z té se vypočte výsledek None a při fázi rozvinování dojde k vynásobení None a hodnoty 0.1:
Traceback (most recent call last): File "./failure1.py", line 8, in <module;> print(factorial(2.1)) File "/home/ptisnovs/src/python/testing-in-python/mypy/factorial2/factorial.py", line 10, in factorial return n * factorial(n-1) File "/home/ptisnovs/src/python/testing-in-python/mypy/factorial2/factorial.py", line 10, in factorial return n * factorial(n-1) File "/home/ptisnovs/src/python/testing-in-python/mypy/factorial2/factorial.py", line 10, in factorial return n * factorial(n-1) TypeError: unsupported operand type(s) for *: 'float' and 'NoneType'
Podobně není ošetřeno zadání zcela nesmyslné hodnoty na vstup, například řetězce:
print(factorial("foo"))
Traceback (most recent call last): File "./failure2.py", line 8, in <module;> print(factorial("foo")) File "/home/ptisnovs/src/python/testing-in-python/mypy/factorial2/factorial.py", line 6, in factorial if n < 0: TypeError: '<' not supported between instances of 'str' and 'int'
Naproti tomu pravdivostní hodnoty True a False jsou v typovém systému Pythonu převedeny na celočíselné hodnoty 1 a 0, takže s nimi výpočet provést lze, i když to vypadá nesmyslně:
print(factorial(True)) print(factorial(False))
Faktoriál True je roven jedné, stejně jako faktoriál False :-)
1 1
10. Explicitní typové kontroly prováděné v čase běhu aplikace
Pochopitelně nám nic nebrání v tom provádět typové kontroly explicitně v čase běhu aplikace. Využít je možné například příkaz assert nebo podmínku zkombinovanou s programovou konstrukcí raise:
"""Výpočet faktoriálu.""" def factorial(n): """Rekurzivní výpočet faktoriálu.""" assert isinstance(n, int), "Integer expected" if n < 0: return None if n == 0: return 1 result = n * factorial(n-1) assert isinstance(result, int), "Internal error in factorial computation" return result
Nyní se chyby detekují již na začátku každého výpočtu (v jeho dalším průběhu sice taktéž probíhají, ale podmínka nemůže být splněna):
print(factorial(2.1)) Traceback (most recent call last): File "./failure1.py", line 8, in <module;> print(factorial(2.1)) File "/home/ptisnovs/src/python/testing-in-python/mypy/factorial3/factorial.py", line 6, in factorial assert isinstance(n, int), "Integer expected" AssertionError: Integer expected
Další pokus:
print(factorial("foo")) Traceback (most recent call last): File "./failure2.py", line 8, in <module;> print(factorial("foo")) File "/home/ptisnovs/src/python/testing-in-python/mypy/factorial3/factorial.py", line 6, in factorial assert isinstance(n, int), "Integer expected" AssertionError: Integer expected
Některé hodnoty, typicky pravdivostní, jsou však typovým systémem Pythonu stále považovány za kladná čísla:
print(factorial(True)) print(factorial(False)) 1 1
11. Použití typové deklarace a kontrola s využitím mypy
Můžeme se samozřejmě pokusit o přidání typových deklarací, a to způsobem, který jsme viděli v úvodních kapitolách. První varianta vychází z toho, že výpočet faktoriálu má cenu provádět pouze pro celočíselné vstupní hodnoty a výsledkem je taktéž celé číslo (až na případy záporných čísel, pro které není faktoriál definován). První varianta typové deklarace by tedy mohla vypadat následovně:
"""Výpočet faktoriálu.""" def factorial(n: int) -> int: """Rekurzivní výpočet faktoriálu.""" if n < 0: return None if n == 0: return 1 return n * factorial(n-1)
Ovšem nástroj mypy v tomto případě zjistí – a to zcela korektně –, že není správně ošetřen případ, kdy funkce vrací nikoli celočíselnou hodnotu, ale hodnotu None:
factorial.py:7: error: Incompatible return value type (got "None", expected "int") Found 1 error in 1 file (checked 1 source file)
12. Pokus o použití typové deklarace používající Union
Vzhledem k tomu, že funkce pro výpočet faktoriálu vrací pro záporné vstupy hodnotu None (s významem „nedefinováno“), mohlo by se zdát, že řešení předchozí chyby je velmi snadné – postačuje použít typovou deklaraci Union[int, None] pro návratovou hodnotu funkce factorial. Další varianta by tedy mohla vypadat následovně:
"""Výpočet faktoriálu.""" from typing import Union def factorial(n: int) -> Union[int, None]: """Rekurzivní výpočet faktoriálu.""" if n < 0: return None if n == 0: return 1 return n * factorial(n-1)
Ovšem ani toto řešení není korektní, protože nyní nástroj mypy zjistí, že v rekurzívním výpočtu by mohlo dojít k vynásobení celého čísla s hodnotou typu None:
factorial.py:12: error: Unsupported operand types for * ("int" and "None") factorial.py:12: note: Right operand is of type "Optional[int]" Found 1 error in 1 file (checked 1 source file)
13. Využití deklarace Optional
Mohlo by se zdát, že se problémy, na které jsme narazili v předchozí kapitole, vyřeší použitím deklarace Optional, zde konkrétně Optional[int]. Ve skutečnosti tomu tak není, protože Optional[typ] je vlastně jen zkrácený zápis Union[typ, None], takže se i přes změnu zdrojového kódu funkce pro výpočet faktoriálu pro mypy nic nezmění. Ostatně se o tom můžeme velmi snadno přesvědčit:
"""Výpočet faktoriálu.""" from typing import Optional def factorial(n: Optional[int]) -> Optional[int]: """Rekurzivní výpočet faktoriálu.""" if n is None: return None if n < 0: return None if n == 0: return 1 return n * factorial(n-1)
Chybová hlášení nástroje mypy zůstávají stejná:
factorial.py:14: error: Unsupported operand types for * ("int" and "None") factorial.py:14: note: Right operand is of type "Optional[int]" Found 1 error in 1 file (checked 1 source file)
14. Úprava těla funkce takovým způsobem, aby byla typově bezpečná
Samozřejmě můžeme jíž ještě dále a přepsat poslední řádek funkce, na němž se – alespoň z pohledu nástroje mypy – míchají datové typy None a int:
return n * factorial(n-1)
Nejprve si necháme vypočítat mezivýsledek:
r = factorial(n-1)
A posléze zkontrolujeme návratovou hodnotu None. Pokud je návratová hodnota jiná – zde mypy správně odvodí, že musí být int – můžeme tuto hodnotu vynásobit s hodnotou n, která je taktéž zaručeně int:
if r is None: return None return n * r
Touto dopomocí a díky odvozovacím mechanismům, které jsou v mypy implementovány, lze tedy provést přepis výpočtu následovně:
"""Výpočet faktoriálu.""" from typing import Optional def factorial(n: Optional[int]) -> Optional[int]: """Rekurzivní výpočet faktoriálu.""" if n is None: return None if n < 0: return None if n == 0: return 1 r = factorial(n-1) if r is None: return None return n * r
V tomto případě již nástroj mypy nebude hlásit žádné chyby, ovšem za cenu toho, že jsme přidali jednu zbytečnou podmínku, která se bude testovat i v runtime.
Success: no issues found in 1 source file
15. Korektní způsob – využití přetížení funkce
Existuje ještě jeden zajímavý způsob, jak zapsat funkci factorial, a to bez nutnosti přidání dalších podmínek vyhodnocovaných v runtime. Tento způsob je založen na přetížení funkce s využitím dekorátoru @overload:
@overload def factorial(n: None) -> None: pass @overload def factorial(n: int) -> int: pass
Tyto funkce ve skutečnosti nebudou nikdy volány (ostatně mají prázdné tělo), ovšem jsou použity nástrojem mypy, který následně bez problémů zkontroluje následující (finální) verzi výpočtu:
"""Výpočet faktoriálu.""" from typing import overload from typing import Optional @overload def factorial(n: None) -> None: pass @overload def factorial(n: int) -> int: pass def factorial(n: Optional[int]) -> Optional[int]: """Rekurzivní výpočet faktoriálu.""" if n is None: return None if n < 0: return None if n == 0: return 1 return n * factorial(n-1)
Výsledek kontroly:
Success: no issues found in 1 source file
16. Jak pracné je přidat informace o typech do složitějších funkcí?
Podívejme se nyní na funkci, s nímž jsme se již v tomto seriálu setkali. Jedná se o funkci nazvanou primes, která dokáže rychlým algoritmem vypočítat prvočísla od 2 do zadaného limitu. Přitom jde o poměrně dlouhý algoritmus (přes dvacet řádků), takže by se mohlo zdát, že přidání typových deklarací bude taktéž komplikované. Ovšem ukazuje se, že tomu tak není, protože je pouze nutné zadat typ vstupního parametru (celočíselný limit) a typ výsledků, což je seznam prvočísel. V Pythonu nemáme koncept čísel bez znaménka, takže se spokojíme s typem int a List[int]:
"""Výpočet seznamu prvočísel až do zadaného limitu.""" from typing import List # originální kód lze nalézt na adrese: # http://www.rosettacode.org/wiki/Sieve_of_Eratosthenes#Odds-only_version_of_the_array_sieve_above def primes(limit: int) -> List[int]: """Výpočet seznamu prvočísel až do zadaného limitu.""" # okrajový případ if limit < 2: return [] # druhý případ - 2 je speciálním prvočíslem if limit < 3: return [2] lmtbf = (limit - 3) // 2 # naplnění tabulky, která se bude prosívat buf = [True] * (lmtbf + 1) # vlastní prosívání for i in range((int(limit ** 0.5) - 3) // 2 + 1): if buf[i]: p = i + i + 3 s = p * (i + 1) + i buf[s::p] = [False] * ((lmtbf - s) // p + 1) # vytvoření seznamu prvočísel return [2] + [i + i + 3 for i, v in enumerate(buf) if v] print(primes(100))
Kontrola by neměla zjistit žádné nekorektní použití typů:
$ mypy primes.py Success: no issues found in 1 source file
17. Specifikace typů kontejnerů
Prakticky v každé aplikaci vyvinuté v Pythonu se setkáme s některými kontejnery, typicky s kontejnery, které jsou součástí samotného programovacího jazyka Python. Jedná se o n-tice (tuple), seznamy list, množiny (set) a slovníky (dict). Typickým znakem Pythonu, resp. přesněji řečeno jeho typového systému, je fakt, že tyto kontejnery mohou obsahovat prvky libovolného typu. To je sice na jednu stranu velmi užitečná vlastnost, ovšem na stranu druhou se v reálných aplikacích vyskytují funkce a metody, které pracují pouze s kontejnery obsahujícími prvky jedinečného typu.
Typickým příkladem může být nějaká funkce, které se předá seznam uživatelů, tedy seznam objektů typu User. Jakým způsobem je však možné specifikovat tento datový typ, tj. jak odlišit seznam prvků libovolného typu od seznamu prvků určitého typu (nebo unie několika typů)? Samozřejmě to možné je a používá se následující způsob zápisu (který jsme již viděli v předchozí kapitole – ovšem tam bez dalšího vysvětlení):
List[int] Dict[int, int] List[User] Dict[int, User]
Podívejme se nyní na poměrně typický příklad použití. Budeme chtít vytvořit funkci vyššího řádu nazvanou apply, která bude aplikovat nějakou funkci typu int → int na seznam typu List[int]. Aplikovaná funkce je (v tom nejjednodušším případě) typu Callable, takže je možné psát:
def apply(function, list): return [function(e) for e in list]
Po přidání informace o typech pak rozšíříme definici funkce následovně:
def apply(function: Callable, list: List[int]) -> List[int]: return [function(e) for e in list]
Příklad použití takto navržené funkce. Nejprve bez typových deklarací:
from typing import overload from typing import Optional @overload def factorial(n: None) -> None: pass @overload def factorial(n: int) -> int: pass def factorial(n: Optional[int]) -> Optional[int]: """Rekurzivní výpočet faktoriálu.""" if n is None: return None if n < 0: return None if n == 0: return 1 return n * factorial(n-1) def apply(function, list): return [function(e) for e in list] print(apply(factorial, [1, 2, 3, 4])) print(apply(factorial, [1, 2.2, "foo", 4]))
A následně s typovými deklaracemi:
from typing import overload from typing import Optional from typing import Callable, List @overload def factorial(n: None) -> None: pass @overload def factorial(n: int) -> int: pass def factorial(n: Optional[int]) -> Optional[int]: """Rekurzivní výpočet faktoriálu.""" if n is None: return None if n < 0: return None if n == 0: return 1 return n * factorial(n-1) def apply(function: Callable, list: List[int]) -> List[int]: return [function(e) for e in list] print(apply(factorial, [1, 2, 3, 4])) print(apply(factorial, [1, 2.2, "foo", 4]))
Kontrola odhalí dvě chyby, kterých jsme se dopustili při volání funkce apply – ta by totiž do funkce factorial předala hodnoty nekorektních typů:
$ mypy apply2.py apply2.py:33: error: List item 1 has incompatible type "float"; expected "int" apply2.py:33: error: List item 2 has incompatible type "str"; expected "int" Found 2 errors in 1 file (checked 1 source file)
Další varianta, tentokrát vytvářející slovník s hodnotou a faktoriálem této hodnoty pomocí nové funkce pojmenované asDict:
from typing import overload from typing import Optional from typing import Callable, List from typing import Dict, Any @overload def factorial(n: None) -> None: pass @overload def factorial(n: int) -> int: pass def factorial(n: Optional[int]) -> Optional[int]: """Rekurzivní výpočet faktoriálu.""" if n is None: return None if n < 0: return None if n == 0: return 1 return n * factorial(n-1) def asDict(function: Callable, list: List[int]) -> Dict[int, int]: return {value: function(value) for value in list} print(asDict(factorial, [1, 2, 3, 4])) print(asDict(factorial, [1, 2.2, "foo", 4]))
Výsledek typové kontroly:
$ mypy apply3.py apply3.py:34: error: List item 1 has incompatible type "float"; expected "int" apply3.py:34: error: List item 2 has incompatible type "str"; expected "int" Found 2 errors in 1 file (checked 1 source file)
18. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/testing-in-python. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady a jejich části, které naleznete v následující tabulce:
19. Předchozí články s tématem testování (nejenom) v Pythonu
Tématem testování jsme se již na stránkách Rootu několikrát zabývali. Jedná se mj. o následující články:
- Použití Pythonu pro tvorbu testů: od jednotkových testů až po testy UI
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-od-jednotkovych-testu-az-po-testy-ui/ - Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-pouziti-tridy-mock-z-knihovny-unittest-mock/ - Použití nástroje pytest pro tvorbu jednotkových testů a benchmarků
https://www.root.cz/clanky/pouziti-nastroje-pytest-pro-tvorbu-jednotkovych-testu-a-benchmarku/ - Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-fixtures-vyjimky-parametrizace-testu/ - Nástroj pytest a jednotkové testy: životní cyklus testů, užitečné tipy a triky
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-zivotni-cyklus-testu-uzitecne-tipy-a-triky/ - Struktura projektů s jednotkovými testy, využití Travis CI
https://www.root.cz/clanky/struktura-projektu-s-jednotkovymi-testy-vyuziti-travis-ci/ - Behavior-driven development v Pythonu s využitím knihovny Behave
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave/ - Behavior-driven development v Pythonu s využitím knihovny Behave (druhá část)
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-druha-cast/ - Behavior-driven development v Pythonu s využitím knihovny Behave (závěrečná část)
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-zaverecna-cast/ - Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-pomoci-knihoven-schemagic-a-schema/ - Validace datových struktur v Pythonu (2. část)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/ - Validace datových struktur v Pythonu (dokončení)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/ - Univerzální testovací nástroj Robot Framework
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/ - Univerzální testovací nástroj Robot Framework a BDD testy
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/ - Úvod do problematiky fuzzingu a fuzz testování
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/ - Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani-slozeni-vlastniho-fuzzeru/ - Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
https://www.root.cz/clanky/knihovny-a-moduly-usnadnujici-testovani-aplikaci-naprogramovanych-v-jazyce-clojure/ - Validace dat s využitím knihovny spec v Clojure 1.9.0
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/ - Testování aplikací naprogramovaných v jazyce Go
https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/ - Knihovny určené pro tvorbu testů v programovacím jazyce Go
https://www.root.cz/clanky/knihovny-urcene-pro-tvorbu-testu-v-programovacim-jazyce-go/ - Testování aplikací psaných v Go s využitím knihoven Goblin a Frisby
https://www.root.cz/clanky/testovani-aplikaci-psanych-v-go-s-vyuzitim-knihoven-goblin-a-frisby/ - Testování Go aplikací s využitím knihovny GΩmega a frameworku Ginkgo
https://www.root.cz/clanky/testovani-go-aplikaci-s-vyuzitim-knihovny-gomega-mega-a-frameworku-ginkgo/ - Tvorba BDD testů s využitím jazyka Go a nástroje godog
https://www.root.cz/clanky/tvorba-bdd-testu-s-vyuzitim-jazyka-go-a-nastroje-godog/ - Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem
https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem/ - Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem (dokončení)
https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem-dokonceni/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/
20. Odkazy na Internetu
- Goto Fail, Heartbleed, and Unit Testing Culture
https://martinfowler.com/articles/testing-culture.html - PEP-484
https://www.python.org/dev/peps/pep-0484/ - In-depth: Functional programming in C++
https://www.gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php - mypy
http://www.mypy-lang.org/ - Welcome to Mypy documentation!
https://mypy.readthedocs.io/en/latest/index.html - mypy na GitHubu
https://github.com/python/mypy - mypy 0.770 na PyPi
https://pypi.org/project/mypy/ - Extensions for mypy (separated out from mypy/extensions)
https://github.com/python/mypy_extensions - The Mypy Blog
https://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html - Our journey to type checking 4 million lines of Python
https://dropbox.tech/application/our-journey-to-type-checking-4-million-lines-of-python - Type-Checking Python Programs With Type Hints and mypy
https://www.youtube.com/watch?v=2×WhaALHTvU - Refactoring to Immutability – Kevlin Henney
https://www.youtube.com/watch?v=APUCMSPiNh4 - Bernat Gabor – Type hinting (and mypy) – PyCon 2019
https://www.youtube.com/watch?v=hTrjTAPnA_k - Stanford Seminar – Optional Static Typing for Python
https://www.youtube.com/watch?v=GiZKuyLKvAA - mypy Getting to Four Million Lines of Typed Python – Michael Sullivan
https://www.youtube.com/watch?v=FT_WHV4-QcU - Shebang
https://en.wikipedia.org/wiki/Shebang_(Unix) - pytest 5.4.2 na PyPi
https://pypi.org/project/pytest/ - Hillel Wayne – Beyond Unit Tests: Taking Your Testing to the Next Level – PyCon 2018
https://www.youtube.com/watch?v=MYucYon2-lk - Awesome Python – testing
https://github.com/vinta/awesome-python#testing - pytest Plugins Compatibility
http://plugincompat.herokuapp.com/ - Selenium (pro Python)
https://pypi.org/project/selenium/ - Getting Started With Testing in Python
https://realpython.com/python-testing/ - unittest.mock — mock object library
https://docs.python.org/3.5/library/unittest.mock.html - mock 2.0.0
https://pypi.python.org/pypi/mock - An Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-python - Mock – Mocking and Testing Library
http://mock.readthedocs.io/en/stable/ - Python Mocking 101: Fake It Before You Make It
https://blog.fugue.co/2016–02–11-python-mocking-101.html - Nauč se Python! – Testování
http://naucse.python.cz/lessons/intro/testing/ - Flexmock (dokumentace)
https://flexmock.readthedocs.io/en/latest/ - Test Fixture (Wikipedia)
https://en.wikipedia.org/wiki/Test_fixture - Mock object (Wikipedia)
https://en.wikipedia.org/wiki/Mock_object - Extrémní programování
https://cs.wikipedia.org/wiki/Extr%C3%A9mn%C3%AD_programov%C3%A1n%C3%AD - Programování řízené testy
https://cs.wikipedia.org/wiki/Programov%C3%A1n%C3%AD_%C5%99%C3%ADzen%C3%A9_testy - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - Tox
https://tox.readthedocs.io/en/latest/ - pytest: helps you write better programs
https://docs.pytest.org/en/latest/ - doctest — Test interactive Python examples
https://docs.python.org/dev/library/doctest.html#module-doctest - unittest — Unit testing framework
https://docs.python.org/dev/library/unittest.html - Python namespaces
https://bytebaker.com/2008/07/30/python-namespaces/ - Namespaces and Scopes
https://www.python-course.eu/namespaces.php - Stránka projektu Robot Framework
https://robotframework.org/ - GitHub repositář Robot Frameworku
https://github.com/robotframework/robotframework - Robot Framework (Wikipedia)
https://en.wikipedia.org/wiki/Robot_Framework - Tutoriál Robot Frameworku
http://www.robotframeworktutorial.com/ - Robot Framework Documentation
https://robotframework.org/robotframework/ - Robot Framework Introduction
https://blog.testproject.io/2016/11/22/robot-framework-introduction/ - robotframework 3.1.2 na PyPi
https://pypi.org/project/robotframework/ - Robot Framework demo (GitHub)
https://github.com/robotframework/RobotDemo - Robot Framework web testing demo using SeleniumLibrary
https://github.com/robotframework/WebDemo - Robot Framework for Mobile Test Automation Demo
https://www.youtube.com/watch?v=06LsU08slP8 - Gherkin
https://cucumber.io/docs/gherkin/ - Selenium
https://selenium.dev/ - SeleniumLibrary
https://robotframework.org/ - The Practical Test Pyramid
https://martinfowler.com/articles/practical-test-pyramid.html - Acceptance Tests and the Testing Pyramid
http://www.blog.acceptancetestdrivendevelopment.com/acceptance-tests-and-the-testing-pyramid/ - Tab-separated values
https://en.wikipedia.org/wiki/Tab-separated_values - A quick guide about Python implementations
https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321 - radamsa
https://gitlab.com/akihe/radamsa - Fuzzing (Wikipedia)
https://en.wikipedia.org/wiki/Fuzzing - american fuzzy lop
http://lcamtuf.coredump.cx/afl/ - Fuzzing: the new unit testing
https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1 - Corpus for github.com/dvyukov/go-fuzz examples
https://github.com/dvyukov/go-fuzz-corpus - AFL – QuickStartGuide.txt
https://github.com/google/AFL/blob/master/docs/QuickStartGuide.txt - Introduction to Fuzzing in Python with AFL
https://alexgaynor.net/2015/apr/13/introduction-to-fuzzing-in-python-with-afl/ - Writing a Simple Fuzzer in Python
https://jmcph4.github.io/2018/01/19/writing-a-simple-fuzzer-in-python/ - How to Fuzz Go Code with go-fuzz (Continuously)
https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/ - Golang Fuzzing: A go-fuzz Tutorial and Example
http://networkbit.ch/golang-fuzzing/ - Fuzzing Python Modules
https://stackoverflow.com/questions/20749026/fuzzing-python-modules - 0×3 Python Tutorial: Fuzzer
http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/ - fuzzing na PyPi
https://pypi.org/project/fuzzing/ - Fuzzing 0.3.2 documentation
https://fuzzing.readthedocs.io/en/latest/ - Randomized testing for Go
https://github.com/dvyukov/go-fuzz - HTTP/2 fuzzer written in Golang
https://github.com/c0nrad/http2fuzz - Ffuf (Fuzz Faster U Fool) – An Open Source Fast Web Fuzzing Tool
https://hacknews.co/hacking-tools/20191208/ffuf-fuzz-faster-u-fool-an-open-source-fast-web-fuzzing-tool.html - Continuous Fuzzing Made Simple
https://fuzzit.dev/ - Halt and Catch Fire
https://en.wikipedia.org/wiki/Halt_and_Catch_Fire#Intel_x86 - Random testing
https://en.wikipedia.org/wiki/Random_testing - Monkey testing
https://en.wikipedia.org/wiki/Monkey_testing - Fuzzing for Software Security Testing and Quality Assurance, Second Edition
https://books.google.at/books?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%22I+settled+on+the+term+fuzz%22&redir_esc=y&hl=de#v=onepage&q=%22I%20settled%20on%20the%20term%20fuzz%22&f=false - libFuzzer – a library for coverage-guided fuzz testing
https://llvm.org/docs/LibFuzzer.html - fuzzy-swagger na PyPi
https://pypi.org/project/fuzzy-swagger/ - fuzzy-swagger na GitHubu
https://github.com/namuan/fuzzy-swagger - Fuzz testing tools for Python
https://wiki.python.org/moin/PythonTestingToolsTaxonomy#Fuzz_Testing_Tools - A curated list of awesome Go frameworks, libraries and software
https://github.com/avelino/awesome-go - gofuzz: a library for populating go objects with random values
https://github.com/google/gofuzz - tavor: A generic fuzzing and delta-debugging framework
https://github.com/zimmski/tavor - hypothesis na GitHubu
https://github.com/HypothesisWorks/hypothesis - Hypothesis: Test faster, fix more
https://hypothesis.works/ - Hypothesis
https://hypothesis.works/articles/intro/ - What is Hypothesis?
https://hypothesis.works/articles/what-is-hypothesis/ - Databáze CVE
https://www.cvedetails.com/ - Fuzz test Python modules with libFuzzer
https://github.com/eerimoq/pyfuzzer - Taof – The art of fuzzing
https://sourceforge.net/projects/taof/ - JQF + Zest: Coverage-guided semantic fuzzing for Java
https://github.com/rohanpadhye/jqf - http2fuzz
https://github.com/c0nrad/http2fuzz - Demystifying hypothesis testing with simple Python examples
https://towardsdatascience.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294 - Testování
http://voho.eu/wiki/testovani/ - Unit testing (Wikipedia.en)
https://en.wikipedia.org/wiki/Unit_testing - Unit testing (Wikipedia.cz)
https://cs.wikipedia.org/wiki/Unit_testing - Unit Test vs Integration Test
https://www.youtube.com/watch?v=0GypdsJulKE - TestDouble
https://martinfowler.com/bliki/TestDouble.html - Test Double
http://xunitpatterns.com/Test%20Double.html - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Acceptance test–driven development
https://en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development - Gauge
https://gauge.org/ - Gauge (software)
https://en.wikipedia.org/wiki/Gauge_(software) - PYPL PopularitY of Programming Language
https://pypl.github.io/PYPL.html - Testing is Good. Pyramids are Bad. Ice Cream Cones are the Worst
https://medium.com/@fistsOfReason/testing-is-good-pyramids-are-bad-ice-cream-cones-are-the-worst-ad94b9b2f05f - Články a zprávičky věnující se Pythonu
https://www.root.cz/n/python/ - PythonTestingToolsTaxonomy
https://wiki.python.org/moin/PythonTestingToolsTaxonomy - Top 6 BEST Python Testing Frameworks [Updated 2020 List]
https://www.softwaretestinghelp.com/python-testing-frameworks/ - pytest-print 0.1.3
https://pypi.org/project/pytest-print/ - pytest fixtures: explicit, modular, scalable
https://docs.pytest.org/en/latest/fixture.html - PyTest Tutorial: What is, Install, Fixture, Assertions
https://www.guru99.com/pytest-tutorial.html - Pytest – Fixtures
https://www.tutorialspoint.com/pytest/pytest_fixtures.htm - Marking test functions with attributes
https://docs.pytest.org/en/latest/mark.html - pytest-print
https://pytest-print.readthedocs.io/en/latest/ - Continuous integration
https://en.wikipedia.org/wiki/Continuous_integration - Travis CI
https://travis-ci.org/