Omezení stavového prostoru testovaných funkcí a metod

2. 6. 2020
Doba čtení: 37 minut

Sdílet

 Autor: Depositphotos
Při psaní jednotkových testů (a vlastně nejenom jich) se většinou snažíme s co nejmenším úsilím otestovat co největší množství kombinací z celého možného stavového prostoru.

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ů

5. Generické funkce

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

20. Odkazy na Internetu

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

Poznámka: termín polymorfismus má ve světě programovacích jazyků (a ještě více v teorii programovacích jazyků) hned několik významů. Polymorfní mohou být operátory (v mnoha jazycích, včetně klasického céčka), funkce, metody i celé třídy. V dnešním článku si vystačíme s polymorfními funkcemi a interně s polymorfními operátory.

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 .
Poznámka: poslední příkaz může být spuštěn i bez sudo a s přepínačem –user, což zajistí instalaci pouze pro aktuálně přihlášeného uživatele.

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
Poznámka: toto je velmi dobrá vlastnost, protože si lze představit situaci, kdy se tento nástroj spustí na zdrojových kódech rozsáhlých projektů. Pokud by se vypsaly (doslova) miliony chyb, asi by nikdo z vývojářů vůbec neměl sílu začít tyto problémy řešit. S mypy se postupuje opačně – typové deklarace se přidávají jen tam, kde jsou z nějakého důvodu zapotřebí a ty lze následně otestovat.

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)
Poznámka: povšimněte si, že mypy v této chvíli již nevypisuje chybu typu „Unsupported operand types for < („int“ and „str“)“, která byla předtím detekována u operandů operátoru <.

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)
Poznámka: opět můžeme vidět, že jsou nalezeny jak nesprávné typy parametrů, tak i použití výsledků ve špatném kontextu (snaha o spojení řetězce a celého čísla atd.)

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
Poznámka: tento přístup není v žádném případě dokonalý a navíc kontroly prováděné za běhu celou aplikaci mohou dosti podstatným způsobem zpomalovat. Proto v dalším textu namísto tohoto přístupu použijeme nástroj mypy a statické kontroly (bez spuštění kódu).

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)
Poznámka: v Pythonu je None současně klíčovým slovem a současně i hodnotou, konkrétně instancí třídy NoneType. Proto také platí relace None==None.

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)
Poznámka: ve skutečnosti k této situaci nemůže dojít, ovšem mypy pouze sleduje použití datových typů a nikoli všechny podmínky a toky řízení v zapsaném algoritmu.

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
Poznámka: asi jste si povšimli, že jsme nástroji mypy trošku zalhali, protože ve skutečnosti se pro záporné vstupní hodnoty (int) vrátí hodnota None a nikoli int.

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]
Poznámka: v předchozím kódu se používá generátorová notace seznamu neboli list comprehension.

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:

bitcoin_skoleni

$ 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)
Poznámka: můžeme vidět, že typové kontroly prováděné nástrojem mypy mohou být užitečné, ovšem pochopitelně nejsou v žádném případě dokonalé (to by vyžadovalo zcela zásadní změny v typovém systému). Ovšem pro některé projekty se může jednat o užitečný pokus o zlepšení celkové kvality produktu. Viz například popis změn provedených ve skutečně obrovském projektu pro zlepšení celkové kvality: Our journey to type checking 4 million lines of Python

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:

# Příklad Stručný popis Cesta
1 comparator1.py implementace funkce compare bez deklarace typů https://github.com/tisnik/testing-in-python/tree/master/mypy/comparator1.py
2 comparator2.py funkce compare akceptující pouze celá čísla https://github.com/tisnik/testing-in-python/tree/master/mypy/comparator2.py
3 comparator3.py funkce compare akceptující kombinaci čísel a řetězců https://github.com/tisnik/testing-in-python/tree/master/mypy/comparator3.py
4 comparator3.py funkce compare akceptující celá čísla nebo řetězce https://github.com/tisnik/testing-in-python/tree/master/mypy/comparator3.py
5 comparator_test.py jednotkové testy pro comparator1.py https://github.com/tisnik/testing-in-python/tree/master/mypy/com­parator_test.py
6 comparator_test2.py hodnoty nan a comparator1.py https://github.com/tisnik/testing-in-python/tree/master/mypy/com­parator_test2.py
       
7 primes.py algoritmus pro výpočet prvočísel, tentokrát s typovými definicemi https://github.com/tisnik/testing-in-python/tree/master/mypy/primes.py
8 adder.py generická funkce pro součet dvou hodnot https://github.com/tisnik/testing-in-python/tree/master/mypy/adder.py
       
9 apply1.py beztypová funkce vyššího řádu typu apply https://github.com/tisnik/testing-in-python/tree/master/mypy/apply1.py
10 apply2.py funkce vyššího řádu apply s typovým omezením https://github.com/tisnik/testing-in-python/tree/master/mypy/apply2.py
11 apply3.py alternativa k funkci apply, která vytváří slovníky https://github.com/tisnik/testing-in-python/tree/master/mypy/apply3.py
       
12 factorial1 původní algoritmus pro výpočet faktoriálu bez informací o typech https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial1
13 factorial2 rozšíření jednotkových testů pro dosažení stoprocentního pokrytí https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial2
14 factorial3 explicitní kontrola typů v čase běhu aplikace (runtime) https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial3
15 factorial4 první varianta algoritmu s typovými deklaracemi https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial4
16 factorial5 druhá varianta povolující hodnoty None https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial5
17 factorial6 třetí varianta využívající Optional https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial6
18 factorial7 oprava předchozího příkladu https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial7
19 factorial8 přetížení funkce pro zajištění typové bezchybnosti https://github.com/tisnik/testing-in-python/tree/master/mypy/factorial8

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:

  1. 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/
  2. 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/
  3. 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/
  4. 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/
  5. 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/
  6. Struktura projektů s jednotkovými testy, využití Travis CI
    https://www.root.cz/clanky/struktura-projektu-s-jednotkovymi-testy-vyuziti-travis-ci/
  7. 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/
  8. 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/
  9. 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/
  10. 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/
  11. Validace datových struktur v Pythonu (2. část)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/
  12. Validace datových struktur v Pythonu (dokončení)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/
  13. Univerzální testovací nástroj Robot Framework
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/
  14. Univerzální testovací nástroj Robot Framework a BDD testy
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/
  15. Úvod do problematiky fuzzingu a fuzz testování
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/
  16. Ú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/
  17. 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/
  18. 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/
  19. Testování aplikací naprogramovaných v jazyce Go
    https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/
  20. 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/
  21. 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/
  22. 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/
  23. 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/
  24. 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/
  25. 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/
  26. 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/
  27. 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

  1. Goto Fail, Heartbleed, and Unit Testing Culture
    https://martinfowler.com/ar­ticles/testing-culture.html
  2. PEP-484
    https://www.python.org/dev/peps/pep-0484/
  3. In-depth: Functional programming in C++
    https://www.gamasutra.com/vi­ew/news/169296/Indepth_Fun­ctional_programming_in_C.php
  4. mypy
    http://www.mypy-lang.org/
  5. Welcome to Mypy documentation!
    https://mypy.readthedocs.i­o/en/latest/index.html
  6. mypy na GitHubu
    https://github.com/python/mypy
  7. mypy 0.770 na PyPi
    https://pypi.org/project/mypy/
  8. Extensions for mypy (separated out from mypy/extensions)
    https://github.com/python/my­py_extensions
  9. The Mypy Blog
    https://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html
  10. Our journey to type checking 4 million lines of Python
    https://dropbox.tech/application/our-journey-to-type-checking-4-million-lines-of-python
  11. Type-Checking Python Programs With Type Hints and mypy
    https://www.youtube.com/wat­ch?v=2×WhaALHTvU
  12. Refactoring to Immutability – Kevlin Henney
    https://www.youtube.com/wat­ch?v=APUCMSPiNh4
  13. Bernat Gabor – Type hinting (and mypy) – PyCon 2019
    https://www.youtube.com/wat­ch?v=hTrjTAPnA_k
  14. Stanford Seminar – Optional Static Typing for Python
    https://www.youtube.com/wat­ch?v=GiZKuyLKvAA
  15. mypy Getting to Four Million Lines of Typed Python – Michael Sullivan
    https://www.youtube.com/wat­ch?v=FT_WHV4-QcU
  16. Shebang
    https://en.wikipedia.org/wi­ki/Shebang_(Unix)
  17. pytest 5.4.2 na PyPi
    https://pypi.org/project/pytest/
  18. Hillel Wayne – Beyond Unit Tests: Taking Your Testing to the Next Level – PyCon 2018
    https://www.youtube.com/wat­ch?v=MYucYon2-lk
  19. Awesome Python – testing
    https://github.com/vinta/awesome-python#testing
  20. pytest Plugins Compatibility
    http://plugincompat.herokuapp.com/
  21. Selenium (pro Python)
    https://pypi.org/project/selenium/
  22. Getting Started With Testing in Python
    https://realpython.com/python-testing/
  23. unittest.mock — mock object library
    https://docs.python.org/3­.5/library/unittest.mock.html
  24. mock 2.0.0
    https://pypi.python.org/pypi/mock
  25. An Introduction to Mocking in Python
    https://www.toptal.com/python/an-introduction-to-mocking-in-python
  26. Mock – Mocking and Testing Library
    http://mock.readthedocs.io/en/stable/
  27. Python Mocking 101: Fake It Before You Make It
    https://blog.fugue.co/2016–02–11-python-mocking-101.html
  28. Nauč se Python! – Testování
    http://naucse.python.cz/les­sons/intro/testing/
  29. Flexmock (dokumentace)
    https://flexmock.readthedoc­s.io/en/latest/
  30. Test Fixture (Wikipedia)
    https://en.wikipedia.org/wi­ki/Test_fixture
  31. Mock object (Wikipedia)
    https://en.wikipedia.org/wi­ki/Mock_object
  32. Extrémní programování
    https://cs.wikipedia.org/wi­ki/Extr%C3%A9mn%C3%AD_pro­gramov%C3%A1n%C3%AD
  33. Programování řízené testy
    https://cs.wikipedia.org/wi­ki/Programov%C3%A1n%C3%AD_%C5%99%­C3%ADzen%C3%A9_testy
  34. Pip (dokumentace)
    https://pip.pypa.io/en/stable/
  35. Tox
    https://tox.readthedocs.io/en/latest/
  36. pytest: helps you write better programs
    https://docs.pytest.org/en/latest/
  37. doctest — Test interactive Python examples
    https://docs.python.org/dev/li­brary/doctest.html#module-doctest
  38. unittest — Unit testing framework
    https://docs.python.org/dev/li­brary/unittest.html
  39. Python namespaces
    https://bytebaker.com/2008/07/30/pyt­hon-namespaces/
  40. Namespaces and Scopes
    https://www.python-course.eu/namespaces.php
  41. Stránka projektu Robot Framework
    https://robotframework.org/
  42. GitHub repositář Robot Frameworku
    https://github.com/robotfra­mework/robotframework
  43. Robot Framework (Wikipedia)
    https://en.wikipedia.org/wi­ki/Robot_Framework
  44. Tutoriál Robot Frameworku
    http://www.robotframeworktu­torial.com/
  45. Robot Framework Documentation
    https://robotframework.or­g/robotframework/
  46. Robot Framework Introduction
    https://blog.testproject.i­o/2016/11/22/robot-framework-introduction/
  47. robotframework 3.1.2 na PyPi
    https://pypi.org/project/ro­botframework/
  48. Robot Framework demo (GitHub)
    https://github.com/robotfra­mework/RobotDemo
  49. Robot Framework web testing demo using SeleniumLibrary
    https://github.com/robotfra­mework/WebDemo
  50. Robot Framework for Mobile Test Automation Demo
    https://www.youtube.com/wat­ch?v=06LsU08slP8
  51. Gherkin
    https://cucumber.io/docs/gherkin/
  52. Selenium
    https://selenium.dev/
  53. SeleniumLibrary
    https://robotframework.org/
  54. The Practical Test Pyramid
    https://martinfowler.com/ar­ticles/practical-test-pyramid.html
  55. Acceptance Tests and the Testing Pyramid
    http://www.blog.acceptance­testdrivendevelopment.com/ac­ceptance-tests-and-the-testing-pyramid/
  56. Tab-separated values
    https://en.wikipedia.org/wiki/Tab-separated_values
  57. A quick guide about Python implementations
    https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321
  58. radamsa
    https://gitlab.com/akihe/radamsa
  59. Fuzzing (Wikipedia)
    https://en.wikipedia.org/wiki/Fuzzing
  60. american fuzzy lop
    http://lcamtuf.coredump.cx/afl/
  61. Fuzzing: the new unit testing
    https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1
  62. Corpus for github.com/dvyukov/go-fuzz examples
    https://github.com/dvyukov/go-fuzz-corpus
  63. AFL – QuickStartGuide.txt
    https://github.com/google/AF­L/blob/master/docs/QuickStar­tGuide.txt
  64. Introduction to Fuzzing in Python with AFL
    https://alexgaynor.net/2015/a­pr/13/introduction-to-fuzzing-in-python-with-afl/
  65. Writing a Simple Fuzzer in Python
    https://jmcph4.github.io/2018/01/19/wri­ting-a-simple-fuzzer-in-python/
  66. How to Fuzz Go Code with go-fuzz (Continuously)
    https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/
  67. Golang Fuzzing: A go-fuzz Tutorial and Example
    http://networkbit.ch/golang-fuzzing/
  68. Fuzzing Python Modules
    https://stackoverflow.com/qu­estions/20749026/fuzzing-python-modules
  69. 0×3 Python Tutorial: Fuzzer
    http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/
  70. fuzzing na PyPi
    https://pypi.org/project/fuzzing/
  71. Fuzzing 0.3.2 documentation
    https://fuzzing.readthedoc­s.io/en/latest/
  72. Randomized testing for Go
    https://github.com/dvyukov/go-fuzz
  73. HTTP/2 fuzzer written in Golang
    https://github.com/c0nrad/http2fuzz
  74. 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
  75. Continuous Fuzzing Made Simple
    https://fuzzit.dev/
  76. Halt and Catch Fire
    https://en.wikipedia.org/wi­ki/Halt_and_Catch_Fire#In­tel_x86
  77. Random testing
    https://en.wikipedia.org/wi­ki/Random_testing
  78. Monkey testing
    https://en.wikipedia.org/wi­ki/Monkey_testing
  79. Fuzzing for Software Security Testing and Quality Assurance, Second Edition
    https://books.google.at/bo­oks?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%­22I+settled+on+the+term+fuz­z%22&redir_esc=y&hl=de#v=o­nepage&q=%22I%20settled%20on%20the%20ter­m%20fuzz%22&f=false
  80. libFuzzer – a library for coverage-guided fuzz testing
    https://llvm.org/docs/LibFuzzer.html
  81. fuzzy-swagger na PyPi
    https://pypi.org/project/fuzzy-swagger/
  82. fuzzy-swagger na GitHubu
    https://github.com/namuan/fuzzy-swagger
  83. Fuzz testing tools for Python
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy#Fuzz_Testing_Tools
  84. A curated list of awesome Go frameworks, libraries and software
    https://github.com/avelino/awesome-go
  85. gofuzz: a library for populating go objects with random values
    https://github.com/google/gofuzz
  86. tavor: A generic fuzzing and delta-debugging framework
    https://github.com/zimmski/tavor
  87. hypothesis na GitHubu
    https://github.com/Hypothe­sisWorks/hypothesis
  88. Hypothesis: Test faster, fix more
    https://hypothesis.works/
  89. Hypothesis
    https://hypothesis.works/ar­ticles/intro/
  90. What is Hypothesis?
    https://hypothesis.works/articles/what-is-hypothesis/
  91. Databáze CVE
    https://www.cvedetails.com/
  92. Fuzz test Python modules with libFuzzer
    https://github.com/eerimoq/pyfuzzer
  93. Taof – The art of fuzzing
    https://sourceforge.net/pro­jects/taof/
  94. JQF + Zest: Coverage-guided semantic fuzzing for Java
    https://github.com/rohanpadhye/jqf
  95. http2fuzz
    https://github.com/c0nrad/http2fuzz
  96. Demystifying hypothesis testing with simple Python examples
    https://towardsdatascience­.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294
  97. Testování
    http://voho.eu/wiki/testovani/
  98. Unit testing (Wikipedia.en)
    https://en.wikipedia.org/wi­ki/Unit_testing
  99. Unit testing (Wikipedia.cz)
    https://cs.wikipedia.org/wi­ki/Unit_testing
  100. Unit Test vs Integration Test
    https://www.youtube.com/wat­ch?v=0GypdsJulKE
  101. TestDouble
    https://martinfowler.com/bli­ki/TestDouble.html
  102. Test Double
    http://xunitpatterns.com/Tes­t%20Double.html
  103. Test-driven development (Wikipedia)
    https://en.wikipedia.org/wiki/Test-driven_development
  104. Acceptance test–driven development
    https://en.wikipedia.org/wi­ki/Acceptance_test%E2%80%93dri­ven_development
  105. Gauge
    https://gauge.org/
  106. Gauge (software)
    https://en.wikipedia.org/wi­ki/Gauge_(software)
  107. PYPL PopularitY of Programming Language
    https://pypl.github.io/PYPL.html
  108. Testing is Good. Pyramids are Bad. Ice Cream Cones are the Worst
    https://medium.com/@fistsOf­Reason/testing-is-good-pyramids-are-bad-ice-cream-cones-are-the-worst-ad94b9b2f05f
  109. Články a zprávičky věnující se Pythonu
    https://www.root.cz/n/python/
  110. PythonTestingToolsTaxonomy
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy
  111. Top 6 BEST Python Testing Frameworks [Updated 2020 List]
    https://www.softwaretestin­ghelp.com/python-testing-frameworks/
  112. pytest-print 0.1.3
    https://pypi.org/project/pytest-print/
  113. pytest fixtures: explicit, modular, scalable
    https://docs.pytest.org/en/la­test/fixture.html
  114. PyTest Tutorial: What is, Install, Fixture, Assertions
    https://www.guru99.com/pytest-tutorial.html
  115. Pytest – Fixtures
    https://www.tutorialspoin­t.com/pytest/pytest_fixtu­res.htm
  116. Marking test functions with attributes
    https://docs.pytest.org/en/la­test/mark.html
  117. pytest-print
    https://pytest-print.readthedocs.io/en/latest/
  118. Continuous integration
    https://en.wikipedia.org/wi­ki/Continuous_integration
  119. Travis CI
    https://travis-ci.org/

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.