Hlavní navigace

Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů

19. 5. 2020
Doba čtení: 51 minut

Sdílet

 Autor: Depositphotos
Ve čtvrté části seriálu o testování s využitím jazyka Python se budeme opět zabývat možnostmi, které vývojářům i testerům nabízí nástroj pytest. Ukážeme si parametrizaci testů, zachycení výjimek i použití takzvaných test fixtures.

Obsah

1. Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů

2. Chování jednotkových testů ve chvíli, kdy testovaná jednotka obsahuje chybu

3. Použití vlastní chybové zprávy ve chvíli, kdy není podmínka splněna

4. Otestování chování funkce pro výpočet průměru pro prázdný vstup

5. Detekce, jestli došlo v testované jednotce k vyhození výjimky určitého typu

6. Objekt nesoucí informace o vyhozené výjimce

7. Parametrizace jednotkových testů

8. Dekorátor @pytest.mark.parametrize

9. Specifikace identifikátorů použitých při konstrukci jména testu

10. Umístění hodnot parametrů přímo do dekorátoru @pytest.mark.parametrize

11. Výpis výjimky, která vznikne v testovaném bloku či v blocích volaných

12. Výběr jednotkových testů pro spuštění na základě jejich jména

13. Výběr jednotkových testů na základě zadané značky

14. Registrace nových značek v souboru pytest.ini

15. Registrace nových značek přímo v jednotkovém testu

16. Test fixtures

17. Obsah následující části seriálu

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. Použití nástroje pytest pro tvorbu jednotkových testů: fixtures, výjimky, životní cyklus testu

V první části článku budeme s využitím jednotkových testů ověřovat funkčnost velmi jednoduché funkce, která pro zadaný vstup (seznam, n-tici) obsahující numerické hodnoty vypočte průměr. Využívá se zde dvojice funkcí, které jsou součástí základní knihovny programovacího jazyka Python, jenž je dostupná ihned po inicializaci a spuštění interpretru. Námi vytvořená funkce se jmenuje average a prozatím nijak netestuje svůj vstup:

"""Výpočet průměru."""

def average(x):
    """Výpočet průměru ze seznamu hodnot předaných v parametru x."""
    return sum(x)/float(len(x))

Základní funkcionalitu modulu average, v němž je stejnojmenná funkce deklarována, můžeme velmi rychle ověřit spuštěním následujícího skriptu a pohledem na standardní výstup:

#!/usr/bin/env python3
 
"""Vstupní bod do testované aplikace."""
 
from average import *
 
 
if __name__ == '__main__':
    # pouze se ujistíme, že lze spustit funkci average
    print(average([1, 2]))

Samozřejmě je ovšem lepší vytvořit si sadu jednotkových testů, které nám umožní ověřovat funkcionalitu a (bez)chybnost automaticky, například při každé ucelenější změně programového kódu (což většinou odpovídá jednomu commitu do repositáře). První verze jednotkových testů (uložená v souboru začínajícím na test_) je prozatím triviální, protože zavolá testovanou funkci jen jedenkrát a zjistí výsledek tohoto volání:

"""Implementace jednotkových testů."""
 
from average import average
 
 
def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    assert result == 1.5

Jednotkové testy můžeme spustit snadno:

$ pytest

S následujícím výsledkem:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average01
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collected 1 item
 
test_average.py .                                                        [100%]
============================== 1 passed in 0.02s ===============================

Osobně ovšem dávám přednost poněkud podrobnějším výsledkům s výpisem všech funkcí a metod, v nichž jsou jednotlivé testy implementovány. Toto chování zajistí přepínač -v:

$ pytest -v

Nyní se zobrazí i jméno testovací funkce, tedy test_average_basic:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average01
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 1 item
 
test_average.py::test_average_basic PASSED                               [100%]
 
============================== 1 passed in 0.02s ===============================

Popř. si můžeme zobrazit, do jaké míry jsou zdrojové kódy pokryty jednotkovými testy:

============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average01
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collected 1 item
 
test_average.py .                                                        [100%]
 
----------- coverage: platform linux, python 3.6.6-final-0 -----------
Name         Stmts   Miss  Cover   Missing
------------------------------------------
average.py       2      0   100%
 
 
============================== 1 passed in 0.04s ===============================
Poznámka: stoprocentní pokrytí ovšem v žádném případě neznamená, že je testovaná jednotka (tedy funkce, metoda či třída) skutečně plně funkční a bezchybná.

První demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average01/.

2. Chování jednotkových testů ve chvíli, kdy testovaná jednotka obsahuje chybu

Podívejme se nyní na to, jak se budou jednotkové testy chovat ve chvíli, kdy se testovaná jednotka (což je v našem případě funkce pro výpočet průměru) v důsledku chyby zanesené programátorem začne chovat nekorektně. Funkce je naschvál upravena tak, že při výpočtu průměru zvyšuje dělitel o jedničku (viz podtržený kód):

"""Výpočet průměru."""
 
def average(x):
    """Výpočet průměru ze seznamu hodnot předaných v parametru x."""
    return sum(x)/float(1+len(x))

Jednotkové testy ovšem zůstanou nezměněny, což zde konkrétně znamená, že pro vstupní hodnoty 1 a 2 (předané prozatím v seznamu nebo n-tici) očekává výsledek 3/2:

"""Implementace jednotkových testů."""
 
from average import average
 
 
def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    assert result == 1.5

Nyní dojde – zcela podle očekávání – k detekci chyby při spuštění jednotkových testů:

$ pytest -v

Zajímavé je zjistit, jak vlastně vypadá výstup z nástroje pytest ve chvíli, kdy k chybě dojde:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average02
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 1 item
 
test_average.py::test_average_basic FAILED                               [100%]
 
=================================== FAILURES ===================================
______________________________ test_average_basic ______________________________
 
    def test_average_basic():
        """Otestování výpočtu průměru."""
        result = average([1, 2])
>       assert result == 1.5
E       assert 1.0 == 1.5
E         +1.0
E         -1.5
 
test_average.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic - assert 1.0 == 1.5
============================== 1 failed in 0.04s ===============================

Povšimněte si, že se vypíše jak vlastní jednotkový test, tak i chyba a to relativně „chytrým“ způsobem: zobrazením aserce (popř. kódu, kde vznikla výjimka) a poté porovnáním očekávaného výsledku a výsledku aktuálního:

>       assert result == 1.5
E       assert 1.0 == 1.5
E         +1.0
E         -1.5

Tyto dvě hodnoty jsou zobrazeny ve formě podobné unifikovanému diffu, což nás může v tomto konkrétním případě mást, protože –1.5 neznamená zápornou hodnotu, ale hodnotu –1.5, před kterou je umístěn znak „-“ znamenající „chybí/je nahrazeno“:

Poznámka: obecně je poměrně špatným nápadem přímo porovnávat dvě numerické hodnoty typu float. Ovšem v tomto konkrétním případě víme (resp. je dobré to vědět), že hodnotu 1.5 lze při použití normy IEEE 754 (kterou Python splňuje) reprezentovat zcela přesně, konkrétně v bajtech s obsahem:
3F F8 00 00 00 00 00 00

Druhý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average02/.

3. Použití vlastní chybové zprávy ve chvíli, kdy není podmínka splněna

V našem demonstračním příkladu je podmínka (resp. přesněji řečeno aserce) umístěná v jednotkovém testu zcela zřejmá a programátorovi by nemělo potíže zjistit, co a proč se testuje. Ovšem společně se stoupajícím množstvím jednotkových testů (a v reálné aplikaci jich mohou být stovky i tisíce) je vhodné čtenářům výsledků testů situaci co nejvíce usnadnit a přidat do testů lidsky čitelnou zprávu. To konstrukce assert umožňuje, protože za podmínkou může být umístěn buď řetězec nebo výraz, který se na řetězec může převést (typicky aplikací šablony). Tento řetězec je použit pro výpis chybové zprávy ve chvíli, kdy podmínka není splněna a tudíž jednotkový test našel potenciální chybu. Jednoduchý příklad:

"""Implementace jednotkových testů."""
 
from average import average
 
 
def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    expected = 1.5
    assert result == expected, "Očekávaná hodnota při výpočtu průměru hodnot 1 a 2 má být {}, vráceno {}".format(expected, result)

Pokud nyní tento jednotkový test spustíme, bude chybové hlášení (vypsáno tučně) obsahovat i kontext, který může uživateli pomoci při zjišťování, k jaké chybě vlastně došlo. Stejný text se zobrazí i v přehledu na konci:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average03
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 1 item
 
test_average.py::test_average_basic FAILED                               [100%]
 
=================================== FAILURES ===================================
______________________________ test_average_basic ______________________________
 
    def test_average_basic():
        """Otestování výpočtu průměru."""
        result = average([1, 2])
        expected = 1.5
>       assert result == expected, "Očekávaná hodnota při výpočtu průměru hodnot 1 a 2 má být {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota při výpočtu průměru hodnot 1 a 2 má být 1.5, vráceno 1.0
E       assert 1.0 == 1.5
E         +1.0
E         -1.5
 
test_average.py:10: AssertionError
================================== short test summary info ========================================
FAILED test_average.py::test_average_basic - AssertionError: Očekávaná hodnota ... 1.5, vráceno 1.0
===================================== 1 failed in 0.04s ===========================================
Poznámka: při přesměrování výsledků testů do souboru se (pokud neprovedete jiné nastavení) používá délka řádku nastavená na 80 znaků, což v našem konkrétním případě znamená, že přehled testů na konci nemusí obsahovat celé chybové hlášení.

Třetí demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average03/.

4. Otestování chování funkce pro výpočet průměru pro prázdný vstup

Sadu jednotkových testů si můžeme velmi snadno rozšířit o další testy zjišťující chování testované funkce v mezních případech. Například by bylo vhodné zjistit, jak se funkce pro výpočet průměru chová ve chvíli, kdy jí nejsou předány žádné hodnoty, resp. přesněji řečeno když se jí předá prázdný seznam nebo prázdná n-tice:

"""Implementace jednotkových testů."""
 
from average import average
 
 
def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    expected = 1.5
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_average_empty_list():
    """Otestování výpočtu průměru."""
    result = average([])
    expected = 0.0
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Nyní jednotkové testy znovu spustíme:

$ pytest -v

A podíváme se na jejich 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average04
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 2 items
 
test_average.py::test_average_basic PASSED                               [ 50%]
test_average.py::test_average_empty_list FAILED                          [100%]
 
=================================== FAILURES ===================================
___________________________ test_average_empty_list ____________________________
 
    def test_average_empty_list():
        """Otestování výpočtu průměru."""
>       result = average([])
 
test_average.py:15:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = []
 
    def average(x):
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
average.py:2: ZeroDivisionError
=========================== short test summary info ============================
FAILED test_average.py::test_average_empty_list - ZeroDivisionError: float di...
========================= 1 failed, 1 passed in 0.04s ==========================

V tomto případě došlo k vyhození výjimky typu ZeroDivisionError, protože se při výpočtu průměru dělí nulou. V navazující kapitole si ukážeme, jakým způsobem je možné tuto výjimku odchytit (pokud budeme považovat toto chování testované funkce za žádoucí).

Čtvrtý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average04/.

5. Detekce, jestli došlo v testované jednotce k vyhození výjimky určitého typu

Knihovna pytest umožňuje velmi elegantním způsobem napsat jednotkový test s detekcí, zda v testované jednotce (v našem případě ve funkci) došlo či naopak nedošlo k vyhození výjimky, a to dokonce výjimky s určitým typem. Pro tento účel se používá řídicí struktura with společně s pytest.raises():

with pytest.raises(ZeroDivisionError) as excinfo:
    result = average([])

V této konstrukci je ZeroDivisionError typ očekávané výjimky a excinfo objekt, který bude obsahovat podrobnější informace o výjimce. Pokud k výjimce skutečně dojde, bude test úspěšný, pokud ovšem nedojde, nahlásí se chyba. Obě tyto možnosti jsou ukázány v dalším zdrojovém kódu jednotkových testů:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    expected = 1.5
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_average_empty_list():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
def test_average_exception_not_raised():
    """Otestování výpočtu průměru."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([1, 2])

První jednotkový test již známe, druhý a třetí pak očekává výjimku. Ovšem jen ve druhém testu skutečně dojde ke vzniku výjimky, což je ostatně patrné i z výsledků běhu:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average05
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 3 items
 
test_average.py::test_average_basic PASSED                               [ 33%]
test_average.py::test_average_empty_list PASSED                          [ 66%]
test_average.py::test_average_exception_not_raised FAILED                [100%]
 
=================================== FAILURES ===================================
______________________ test_average_exception_not_raised _______________________
 
    def test_average_exception_not_raised():
        """Otestování výpočtu průměru."""
        with pytest.raises(ZeroDivisionError) as excinfo:
>           result = average([1, 2])
E           Failed: DID NOT RAISE <class 'ZeroDivisionError'>
 
test_average.py:24: Failed
=========================== short test summary info ============================
FAILED test_average.py::test_average_exception_not_raised - Failed: DID NOT R...
========================= 1 failed, 2 passed in 0.04s ==========================

Pátý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average05/.

6. Objekt nesoucí informace o vyhozené výjimce

V programové konstrukci with, v níž se očekává vyhození výjimky, se mj. při splnění podmínky (výjimka vznikla) naplní i proměnná, kterou jsme pojmenovali excinfo:

with pytest.raises(Exception) as excinfo:
    result = average([])

Tato proměnná obsahuje několik důležitých údajů o výjimce uložených ve formě běžných atributů objektu, zejména pak:

  1. type
  2. value
  3. traceback

Můžeme tedy psát (což je ovšem velmi umělá konstrukce) například:

with pytest.raises(Exception) as excinfo:
    result = average([])
# poměrně křehký způsob testování!
assert excinfo.type == ZeroDivisionError
assert str(excinfo.value) == "float division by zero"

Na druhou stranu je někdy nutné testovat i hodnotu předávanou v objektu představujícím výjimku:

with pytest.raises(ZeroDivisionError) as excinfo:
    result = average([])
assert str(excinfo.value) == "float division by zero"

V šestém příkladu budou všechny testy provedeny, a to aniž by byla detekována chyba v testované funkci:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    expected = 1.5
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_average_empty_list_1():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
def test_average_empty_list_2():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(Exception) as excinfo:
        result = average([])
    # poměrně křehký způsob testování!
    assert excinfo.type == ZeroDivisionError
    assert str(excinfo.value) == "float division by zero"

Výsledek běhu jednotkových testů:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average06
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 3 items
 
test_average.py::test_average_basic PASSED                               [ 33%]
test_average.py::test_average_empty_list_1 PASSED                        [ 66%]
test_average.py::test_average_empty_list_2 PASSED                        [100%]
 
============================== 3 passed in 0.02s ===============================

Šestý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average06/.

7. Parametrizace jednotkových testů

Představme si, že budeme chtít rozšířit následující jednotkový test o další vstupní hodnoty a o očekávané výsledky:

def test_average_basic():
    """Otestování výpočtu průměru."""
    result = average([1, 2])
    expected = 1.5
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Pochopitelně je možné vytvořit například tento kód:

def test_average_basic_more_checks():
    """Otestování výpočtu průměru."""
    inputs = (
            (0, 0),
            (1, 0),
            (1, 1),
            (1, 2))
    expected_results = (0, 0.5, 1, 1.5)
 
    for input, expected in zip(inputs, expected_results):
        result = average(input)
        assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Nejedná se ovšem o příliš elegantní ani idiomatický způsob, jak to udělat, protože se přímo v jednotkovém testu kombinuje jak vlastní test, tak i pomocná konstrukce se smyčkou atd. V následujících kapitolách si ukážeme jiné způsoby.

Poznámka: na druhou stranu se ovšem tento způsob používá například v programovacím jazyce Go, kde je naopak považován za idiomatický:
type AddTest struct {
        x        int32
        y        int32
        expected int32
}
 
func checkAdd(t *testing.T, testInputs []AddTest) {
        for _, i := range testInputs {
                result := add(i.x, i.y)
                if result != i.expected {
                        msg := fmt.Sprintf("%d + %d should be %d, got %d instead",
                                i.x, i.y, i.expected, result)
                        t.Error(msg)
                }
        }
}
 
func TestAddBasicValues(t *testing.T) {
        var addTestInput = []AddTest{
                {0, 0, 0},
                {1, 0, 1},
                {2, 0, 2},
                {2, 1, 3},
        }
        checkAdd(t, addTestInput)
}

8. Dekorátor @pytest.mark.parametrize

Ukažme si nyní poněkud lepší způsob, jak zapsat jednotkový test bez nutnosti explicitního procházení seznamem/n-ticí vstupů a očekávaných výsledků. Naši funkci s testem upravíme takovým způsobem, aby akceptovala dva parametry – vstup do testované funkce a její očekávaný výsledek:

def test_average_basic(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Data pro tento test mohou být reprezentována například n-ticí obsahující další n-tice; zde konkrétně dvojice vstup+očekávaný výsledek:

testdata = [
        ((1, 1), 1),
        ((1, 2), 1.5),
        ((0, 1), 0.5),
        ((1, 2, 3), 2.0),
        ((0, 10), 0.5),
]

Zbývá nám jediné – uvést dekorátor @pytest.mark.parametrize před hlavičku testu:

@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic(values, expected):
    ...
    ...
    ...

Vlastní postupné doplnění parametrů zajistí nástroj pytest. Upravený kód jednotkových testů tedy může vypadat následovně:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
testdata = [
        ((1,1), 1),
        ((1,2), 1.5),
        ((0,1), 0.5),
        ((1,2,3), 2.0),
        ((0,10), 0.5),
]
 
 
@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_average_empty_list_1():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
def test_average_empty_list_2():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(Exception) as excinfo:
        result = average([])
    # poměrně křehký způsob testování!
    assert excinfo.type == ZeroDivisionError
    assert str(excinfo.value) == "float division by zero"

Z výsledků je patrné, že se vytvořily nové testy rozlišené hodnotou v hranatých závorkách (viz zvýrazněný text):

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average07
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 7 items
 
test_average.py::test_average_basic[values0-1] PASSED                    [ 14%]
test_average.py::test_average_basic[values1-1.5] PASSED                  [ 28%]
test_average.py::test_average_basic[values2-0.5] PASSED                  [ 42%]
test_average.py::test_average_basic[values3-2.0] PASSED                  [ 57%]
test_average.py::test_average_basic[values4-0.5] FAILED                  [ 71%]
test_average.py::test_average_empty_list_1 PASSED                        [ 85%]
test_average.py::test_average_empty_list_2 PASSED                        [100%]
 
=================================== FAILURES ===================================
_______________________ test_average_basic[values4-0.5] ________________________
 
values = (0, 10), expected = 0.5
 
    @pytest.mark.parametrize("values,expected", testdata)
    def test_average_basic(values, expected):
        """Otestování výpočtu průměru."""
        result = average(values)
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
test_average.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic[values4-0.5] - AssertionError: Oče...
========================= 1 failed, 6 passed in 0.05s ==========================

Sedmý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average07/.

9. Specifikace identifikátorů použitých při konstrukci jména testu

V předchozím výpisu výsledku testů jsme mohli vidět, že se testovací framework pytest pokusil o vytvoření jména testů, do nichž automaticky dosazoval parametry:

test_average.py::test_average_basic[values0-1] PASSED                    [ 14%]
test_average.py::test_average_basic[values1-1.5] PASSED                  [ 28%]
test_average.py::test_average_basic[values2-0.5] PASSED                  [ 42%]
test_average.py::test_average_basic[values3-2.0] PASSED                  [ 57%]
test_average.py::test_average_basic[values4-0.5] FAILED                  [ 71%]

Tato jména (identifikátory) je ovšem možné specifikovat i přímo v kódu testů, což se v některých případech hodí (například při testování funkce pro login můžeme označit správné a nesprávné heslo atd.). Pro tento účel lze specifikovat atribut ids v dekorátoru @pytest.mark.parametrize:

@pytest.mark.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
def test_average_basic_2(values, expected):
    ...
    ...
    ...

Ve výsledku se objeví odlišná jména, která více odpovídají tomu, co vlastně testujeme:

test_average.py::test_average_basic_2[1,1] PASSED                        [ 14%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 28%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 42%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 57%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 71%]

Pro úplnost se podívejme na úplný zdrojový kód testů:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
testdata = [
        ((1,1), 1),
        ((1,2), 1.5),
        ((0,1), 0.5),
        ((1,2,3), 2.0),
        ((0,10), 0.5),
]
 
 
@pytest.mark.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
def test_average_basic_2(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_average_empty_list_1():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
def test_average_empty_list_2():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(Exception) as excinfo:
        result = average([])
    # poměrně křehký způsob testování!
    assert excinfo.type == ZeroDivisionError
    assert str(excinfo.value) == "float division by zero"

Spuštění jednotkových testů opět provedeme s přepínačem -v:

$ pytest -v

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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average08
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 7 items
 
test_average.py::test_average_basic_2[1,1] PASSED                        [ 14%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 28%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 42%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 57%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 71%]
test_average.py::test_average_empty_list_1 PASSED                        [ 85%]
test_average.py::test_average_empty_list_2 PASSED                        [100%]
 
=================================== FAILURES ===================================
__________________________ test_average_basic_2[0,10] __________________________
 
values = (0, 10), expected = 0.5
 
    @pytest.mark.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
    def test_average_basic_2(values, expected):
        """Otestování výpočtu průměru."""
        result = average(values)
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
test_average.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic_2[0,10] - AssertionError: Očekávan...
========================= 1 failed, 6 passed in 0.05s ==========================

Osmý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average08/.

10. Umístění hodnot parametrů přímo do dekorátoru @pytest.mark.parametrize

Alternativně je možné hodnoty parametrů, které se mají předávat do jednotkového testu, zadat přímo do dekorátoru @pytest.mark.parametrize. Povšimněte si, že se v tomto případě nejdříve uvede mapování mezi hodnotami a parametry testu a následuje seznam nebo n-tice objektů typu pytest.param. Celý zápis je tedy nepatrně delší, ovšem umožňuje, aby se celý programový kód související s jedním jednotkovým testem spojil i s testovacími daty:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
@pytest.mark.parametrize(
    "values,expected",
    [
        pytest.param(
            (1, 1), 1
        ),
        pytest.param(
            (1, 2), 1.5
        ),
        pytest.param(
            (0, 1), 0.5
        ),
        pytest.param(
            (1, 2, 3), 2.0
        ),
        pytest.param(
            (0, 10), 0.5
        ),
    ],
)
def test_average_basic_3(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
def test_average_empty_list_1():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
def test_average_empty_list_2():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(Exception) as excinfo:
        result = average([])
    # poměrně křehký způsob testování!
    assert excinfo.type == ZeroDivisionError
    assert str(excinfo.value) == "float division by zero"

Při pohledu na výsledky jednotkových testů je patrné, že se změnila jména automaticky vytvářených testů:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average09
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 7 items
 
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 14%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 28%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 42%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 57%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 71%]
test_average.py::test_average_empty_list_1 PASSED                        [ 85%]
test_average.py::test_average_empty_list_2 PASSED                        [100%]
 
=================================== FAILURES ===================================
______________________ test_average_basic_3[values4-0.5] _______________________
 
values = (0, 10), expected = 0.5
 
    @pytest.mark.parametrize(
        "values,expected",
        [
            pytest.param(
                (1,1), 1
            ),
            pytest.param(
                (1,2), 1.5
            ),
            pytest.param(
                (0,1), 0.5
            ),
            pytest.param(
                (1,2,3), 2.0
            ),
            pytest.param(
                (0,10), 0.5
            ),
        ],
    )
    def test_average_basic_3(values, expected):
        """Otestování výpočtu průměru."""
        result = average(values)
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
test_average.py:31: AssertionError
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic_3[values4-0.5] - AssertionError: O...
========================= 1 failed, 6 passed in 0.05s ==========================

Devátý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average09/.

Alternativní zápis s n-ticí namísto seznamu (což je z hlediska sémantiky lepší řešení):

@pytest.mark.parametrize(
    "values,expected",
    (
        pytest.param(
            (1, 1), 1
        ),
        pytest.param(
            (1, 2), 1.5
        ),
        pytest.param(
            (0, 1), 0.5
        ),
        pytest.param(
            (1, 2, 3), 2.0
        ),
        pytest.param(
            (0, 10), 0.5
        ),
    ),
)
def test_average_basic_3(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

11. Výpis výjimky, která vznikne v testovaném bloku či v blocích volaných

Nyní funkci pro výpočet průměru upravíme – a to velmi umělým způsobem. Zařídíme totiž, že se vlastní výpočet bude provádět ve funkci nazvané f4, ovšem sekvence volání pro získání kýženého výsledku bude složitější: average→f1→f2→f3→f4:výpočet. Vyzkoušíme si tak, jakým způsobem bude nástroj pytest reagovat na situaci, kdy výjimka vznikne až ve funkci f4, která se nikde z jednotkových testů přímo nevolá:

"""Výpočet průměru špagetovým kódem."""
 
 
def average(x):
    """Výpočet průměru ze seznamu hodnot předaných v parametru x."""
    return f1(x)
 
 
def f1(x):
    return f2(x)
 
 
def f2(x):
    return f3(x)
 
 
def f3(x):
    return f4(x)
 
 
def f4(x):
    return sum(x)/float(len(x))

V jednotkových testech kromě jiného zjišťujeme, co se stane ve chvíli, kdy se má vypočítat průměr z prázdného seznamu nebo prázdné n-tice. Tehdy je vyhozena výjimka, která „probublá“ z funkce nazvané f4 přes funkci average až do vlastního jednotkového testu:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average10
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 18 items
 
test_average.py::test_average_basic_1[values0-1] PASSED                  [  5%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                [ 11%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                [ 16%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                [ 22%]
test_average.py::test_average_basic_1[values4-0.5] FAILED                [ 27%]
test_average.py::test_average_basic_2[1,1] PASSED                        [ 33%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 38%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 44%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 50%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 55%]
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 61%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 66%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 72%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 77%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 83%]
test_average.py::test_average_basic_3[values5-0] FAILED                  [ 88%]
test_average.py::test_average_empty_list_1 PASSED                        [ 94%]
test_average.py::test_average_empty_list_2 PASSED                        [100%]
 
=================================== FAILURES ===================================
______________________ test_average_basic_1[values4-0.5] _______________________
...
...
...
__________________________ test_average_basic_2[0,10] __________________________
...
...
...
______________________ test_average_basic_3[values4-0.5] _______________________
...
...
...
_______________________ test_average_basic_3[values5-0] ________________________
...
...
...
    def test_average_basic_3(values, expected):
        """Otestování výpočtu průměru."""
>       result = average(values)
 
test_average.py:56:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
average.py:5: in average
    return f1(x)
average.py:8: in f1
    return f2(x)
average.py:11: in f2
    return f3(x)
average.py:14: in f3
    return f4(x)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f4(x):
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
average.py:17: ZeroDivisionError
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic_1[values4-0.5] - AssertionError: O...
FAILED test_average.py::test_average_basic_2[0,10] - AssertionError: Očekávan...
FAILED test_average.py::test_average_basic_3[values4-0.5] - AssertionError: O...
FAILED test_average.py::test_average_basic_3[values5-0] - ZeroDivisionError: ...
========================= 4 failed, 14 passed in 0.09s =========================

Povšimněte si, že informaci o výjimce dostaneme s prakticky celým výpisem zásobníkových rámců, což pochopitelně zjednodušuje zjišťování kde a proč vlastně k výjimce došlo:

test_average.py:56:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
average.py:5: in average
    return f1(x)
average.py:8: in f1
    return f2(x)
average.py:11: in f2
    return f3(x)
average.py:14: in f3
    return f4(x)

Desátý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average10/.

12. Výběr jednotkových testů pro spuštění na základě jejich jména

Pokud jsou dodrženy nějaké jednotné konvence pojmenování funkcí (nebo metod) s realizací jednotlivých jednotkových testů, je možné na příkazové řádce specifikovat, které testy se mají spustit. Výběr testů může být proveden na základě části jejich jména, což je varianta, kterou si ukážeme v této kapitole. Testy, které zadanému vzoru neodpovídají, jsou přeskočeny.

Spuštění testů, v jejichž jménu se objevuje slovo „basic“:

$ pytest -v -k basic

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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average11
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 18 items / 2 deselected / 16 selected
 
test_average.py::test_average_basic_1[values0-1] PASSED                  [  6%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                [ 12%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                [ 18%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                [ 25%]
test_average.py::test_average_basic_1[values4-0.5] FAILED                [ 31%]
test_average.py::test_average_basic_2[1,1] PASSED                        [ 37%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 43%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 50%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 56%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 62%]
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 68%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 75%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 81%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 87%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 93%]
test_average.py::test_average_basic_3[values5-0] FAILED                  [100%]
 
=================================== FAILURES ===================================
______________________ test_average_basic_1[values4-0.5] _______________________
 
values = (0, 10), expected = 0.5
 
    @pytest.mark.smoketest
    @pytest.mark.parametrize("values,expected", testdata)
    def test_average_basic_1(values, expected):
        """Otestování výpočtu průměru."""
        result = average(values)
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
test_average.py:28: AssertionError
__________________________ test_average_basic_2[0,10] __________________________
 
values = (0, 10), expected = 0.5
 
    @pytest.mark.smoketest
    @pytest.mark.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
    def test_average_basic_2(values, expected):
        """Otestování výpočtu průměru."""
        result = average(values)
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
test_average.py:36: AssertionError
______________________ test_average_basic_3[values4-0.5] _______________________
 
values = (0, 10), expected = 0.5
 
    @pytest.mark.smoketest
    @pytest.mark.parametrize(
        "values,expected",
        [
            pytest.param(
                (1, 1), 1
            ),
            pytest.param(
                (1, 2), 1.5
            ),
            pytest.param(
                (0, 1), 0.5
            ),
            pytest.param(
                (1, 2, 3), 2.0
            ),
            pytest.param(
                (0, 10), 0.5
            ),
            pytest.param(
                (), 0
            ),
        ],
    )
    def test_average_basic_3(values, expected):
        """Otestování výpočtu průměru."""
        result = average(values)
>       assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
test_average.py:66: AssertionError
_______________________ test_average_basic_3[values5-0] ________________________
 
values = (), expected = 0
 
    @pytest.mark.smoketest
    @pytest.mark.parametrize(
        "values,expected",
        [
            pytest.param(
                (1, 1), 1
            ),
            pytest.param(
                (1, 2), 1.5
            ),
            pytest.param(
                (0, 1), 0.5
            ),
            pytest.param(
                (1, 2, 3), 2.0
            ),
            pytest.param(
                (0, 10), 0.5
            ),
            pytest.param(
                (), 0
            ),
        ],
    )
    def test_average_basic_3(values, expected):
        """Otestování výpočtu průměru."""
>       result = average(values)
 
test_average.py:65:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
average.py:6: in average
    return f1(x)
average.py:10: in f1
    return f2(x)
average.py:14: in f2
    return f3(x)
average.py:18: in f3
    return f4(x)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f4(x):
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
average.py:22: ZeroDivisionError
=============================== warnings summary ===============================
test_average.py:23
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:23: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:31
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:31: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:39
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:39: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:69
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:69: PytestUnknownMarkWarning: Unknown pytest.mark.thorough - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.thorough
 
test_average.py:76
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:76: PytestUnknownMarkWarning: Unknown pytest.mark.thorough - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.thorough
 
-- Docs: https://docs.pytest.org/en/latest/warnings.html
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic_1[values4-0.5] - AssertionError: O...
FAILED test_average.py::test_average_basic_2[0,10] - AssertionError: Očekávan...
FAILED test_average.py::test_average_basic_3[values4-0.5] - AssertionError: O...
FAILED test_average.py::test_average_basic_3[values5-0] - ZeroDivisionError: ...
============ 4 failed, 12 passed, 2 deselected, 5 warnings in 0.08s ============

Spuštění testů, v jejichž jménu se objevuje slovo „empty“:

$ pytest -v -k empty
============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average11
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 18 items / 16 deselected / 2 selected
 
test_average.py::test_average_empty_list_1 PASSED                        [ 50%]
test_average.py::test_average_empty_list_2 PASSED                        [100%]
 
=============================== warnings summary ===============================
test_average.py:23
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:23: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:31
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:31: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:39
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:39: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:69
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:69: PytestUnknownMarkWarning: Unknown pytest.mark.thorough - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.thorough
 
test_average.py:76
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:76: PytestUnknownMarkWarning: Unknown pytest.mark.thorough - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.thorough
 
-- Docs: https://docs.pytest.org/en/latest/warnings.html
================= 2 passed, 16 deselected, 5 warnings in 0.02s =================

Spuštění testů, v jejichž jménu se objevuje slovo „unknown“:

$ pytest -v -k unknown

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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average11
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 18 items / 18 deselected
 
=============================== warnings summary ===============================
test_average.py:23
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:23: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:31
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:31: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:39
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:39: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest
 
test_average.py:69
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:69: PytestUnknownMarkWarning: Unknown pytest.mark.thorough - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.thorough
 
test_average.py:76
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:76: PytestUnknownMarkWarning: Unknown pytest.mark.thorough - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.thorough
 
-- Docs: https://docs.pytest.org/en/latest/warnings.html
====================== 18 deselected, 5 warnings in 0.02s ======================
Poznámka: důvodem vzniku varování se budeme zabývat v dalších kapitolách.

Jedenáctý demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average11/.

13. Výběr jednotkových testů na základě zadané značky

Jednotkové testy je možné vybrat i na základě zadané značky. Ta se opět specifikuje dekorátorem a to například následujícím způsobem:

@pytest.mark.smoketest
@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic_1(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

V tomto případě je smoketest značkou, s jejímž využitím je možné test či testy vybírat. Takové značky pochopitelně můžeme dopsat i k dalším testům:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
def pytest_configure(config):
    """Konfigurace jednotkových testů."""
    config.addinivalue_line(
        "markers", "smoketest: mark test that are performed very smoketest"
    )
 
 
testdata = [
        ((1, 1), 1),
        ((1, 2), 1.5),
        ((0, 1), 0.5),
        ((1, 2, 3), 2.0),
        ((0, 10), 0.5),
]
 
 
@pytest.mark.smoketest
@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic_1(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
@pytest.mark.smoketest
@pytest.mark.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
def test_average_basic_2(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
@pytest.mark.smoketest
@pytest.mark.parametrize(
    "values,expected",
    [
        pytest.param(
            (1, 1), 1
        ),
        pytest.param(
            (1, 2), 1.5
        ),
        pytest.param(
            (0, 1), 0.5
        ),
        pytest.param(
            (1, 2, 3), 2.0
        ),
        pytest.param(
            (0, 10), 0.5
        ),
        pytest.param(
            (), 0
        ),
    ],
)
def test_average_basic_3(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
@pytest.mark.thorough
def test_average_empty_list_1():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
@pytest.mark.thorough
def test_average_empty_list_2():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(Exception) as excinfo:
        result = average([])
    # poměrně křehký způsob testování!
    assert excinfo.type == ZeroDivisionError
    assert str(excinfo.value) == "float division by zero"

Značka se při výběru testů pro spouštění zadává za přepínačem -m:

$ pytest -v -m smoketest
$ pytest -v -m thorough
$ pytest -v -m other

Příklad výsledků si uvedeme pouze pro první možnost, tj. pro testy označené značkou smoketest:

============================= 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
benchmark: 3.2.3 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
rootdir: /home/ptisnovs/src/python/testing-in-python/pytest/average11
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 18 items / 2 deselected / 16 selected
 
test_average.py::test_average_basic_1[values0-1] PASSED                  [  6%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                [ 12%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                [ 18%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                [ 25%]
test_average.py::test_average_basic_1[values4-0.5] FAILED                [ 31%]
test_average.py::test_average_basic_2[1,1] PASSED                        [ 37%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 43%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 50%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 56%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 62%]
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 68%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 75%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 81%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 87%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 93%]
test_average.py::test_average_basic_3[values5-0] FAILED                  [100%]
...
...
...
=========================== short test summary info ============================
FAILED test_average.py::test_average_basic_1[values4-0.5] - AssertionError: O...
FAILED test_average.py::test_average_basic_2[0,10] - AssertionError: Očekávan...
FAILED test_average.py::test_average_basic_3[values4-0.5] - AssertionError: O...
FAILED test_average.py::test_average_basic_3[values5-0] - ZeroDivisionError: ...
============ 4 failed, 12 passed, 2 deselected, 5 warnings in 0.09s ============

14. Registrace nových značek v souboru pytest.ini

V předchozích výpisech provedených nástrojem pytest jste si mohli povšimnout zpráv s varováním o neznámých značkách:

test_average.py:23
  /home/ptisnovs/src/python/testing-in-python/pytest/average11/test_average.py:23: PytestUnknownMarkWarning: Unknown pytest.mark.smoketest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.smoketest

Varování je zřejmé – nástroj pytest nalezl několik dekorátorů které nezná, takže uživatele upozorňuje na potenciální chyby. Řešení tohoto problému je dvojí. Buď se vytvoří konfigurační soubor nazvaný pytest.ini, který bude tyto značky obsahovat:

[pytest]
markers =
    smoketest
    thorough

Dvanáctý demonstrační příklad, který tento soubor obsahuje, naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average12/.

15. Registrace nových značek přímo v jednotkovém testu

Alternativně k řešení popsaném v předchozí kapitole je možné přímo v jednotkovém testu vytvořit funkci nazvanou pytest_configure, která je součástí životního cyklu testů, o němž budeme mluvit příště. V této funkci lze do objektu config s konfigurací jednotkových testů přidat další řádek nebo řádky ve formátu:

def pytest_configure(config):
    """Konfigurace jednotkových testů."""
    config.addinivalue_line(
        "markers", "smoketest: mark test that are performed very smoketest"
    )

Třináctý demonstrační příklad, který takto upravený test obsahuje, naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average13/.

16. Test fixtures

Povídání o nástroji pytest by v žádném případě nebylo úplné, pokud bychom se nezmínili o takzvaných test fixtures (popravdě nevím, jestli vůbec existuje rozumný překlad tohoto sousloví). Zjednodušeně řečeno se jedná o funkce, které dokážou jednotkovým testům vygenerovat data, připravit kontext, v němž se testy spouští (zajištění připojení do skutečné či mockované databáze…), popř. připravit nějaké pomocné objekty použité v testech. Výhodou fixtures je jejich oddělenost od jednotlivých testů, částečná izolovanost, díky nim lze omezit použití globálních objektů apod.

V tom nejjednodušším případě je test fixture skutečně realizován jednoduchou funkcí, v prvním případě funkcí vracející n-tici s hodnotami použitými v testu:

@pytest.fixture
def input_values():
    """Vygenerování vstupních hodnot pro jednotkový test."""
    return (1, 2, 3, 4, 5)

Druhý test fixture je ještě jednodušší, neboť se jedná o funkci vracející jedinou hodnotu:

@pytest.fixture
def expected_result():
    """Vygenerování očekávaného výsledku testu."""
    return 3

Nejzajímavější je ovšem způsob použití obou výše uvedených test fixtures. Ty totiž můžeme předat přímo zvolenému jednotkovému testu ve formě parametrů – jména parametrů v tomto případě odpovídají jménům funkcí, které test fixtures realizují (ve skutečnosti je inicializace fixtures, jejich zavolání a předání součástí projektu pytest a neprovádí se tedy přímo). Příklad použití je následující:

def test_average_five_values(input_values, expected_result):
    """Otestování výpočtu průměru."""
    result = average(input_values)
    assert result == expected_result, "Očekávaná hodnota {}, vráceno {}".format(expected_result, result)

Do takto realizovaného jednotkového testu se při spuštění pytest přenese n-tice (1, 2, 3, 4, 5) a taktéž hodnota 3. Jedná se skutečně o jeden z nejjednodušších způsobů použití test fixtures, ovšem příště si uvedeme i složitější a realističtější příklady.

Úplný zdrojový kód s jednotkovými testy vypadá po úpravách následovně:

"""Implementace jednotkových testů."""
 
import pytest
 
from average import average
 
 
def pytest_configure(config):
    """Konfigurace jednotkových testů."""
    config.addinivalue_line(
        "markers", "smoketest: mark test that are performed very smoketest"
    )
 
 
testdata = [
        ((1, 1), 1),
        ((1, 2), 1.5),
        ((0, 1), 0.5),
        ((1, 2, 3), 2.0),
        ((0, 10), 0.5),
]
 
 
@pytest.mark.smoketest
@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic_1(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
@pytest.mark.smoketest
@pytest.mark.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
def test_average_basic_2(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
@pytest.mark.smoketest
@pytest.mark.parametrize(
    "values,expected",
    [
        pytest.param(
            (1, 1), 1
        ),
        pytest.param(
            (1, 2), 1.5
        ),
        pytest.param(
            (0, 1), 0.5
        ),
        pytest.param(
            (1, 2, 3), 2.0
        ),
        pytest.param(
            (0, 10), 0.5
        ),
        pytest.param(
            (), 0
        ),
    ],
)
def test_average_basic_3(values, expected):
    """Otestování výpočtu průměru."""
    result = average(values)
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)
 
 
@pytest.mark.thorough
def test_average_empty_list_1():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(ZeroDivisionError) as excinfo:
        result = average([])
 
 
@pytest.mark.thorough
def test_average_empty_list_2():
    """Otestování výpočtu průměru pro prázdný vstup."""
    with pytest.raises(Exception) as excinfo:
        result = average([])
    # poměrně křehký způsob testování!
    assert excinfo.type == ZeroDivisionError
    assert str(excinfo.value) == "float division by zero"
 
 
@pytest.fixture
def input_values():
    """Vygenerování vstupních hodnot pro jednotkový test."""
    return (1, 2, 3, 4, 5)
 
 
@pytest.fixture
def expected_result():
    """Vygenerování očekávaného výsledku testu."""
    return 3
 
 
def test_average_five_values(input_values, expected_result):
    """Otestování výpočtu průměru."""
    result = average(input_values)
    assert result == expected_result, "Očekávaná hodnota {}, vráceno {}".format(expected_result, result)

Čtrnáctý a dnes i poslední demonstrační příklad naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/pytest/average14/.

17. Obsah následující části seriálu

V navazující části seriálu o testování s využitím programovacího jazyka Python se do větších detailů podíváme na různé způsoby použití fixtures. Taktéž se budeme podrobněji zabývat životním cyklem testů, použitím asercí a některými dalšími zajímavými pluginy vyvinutými pro nástroj pytest.

root_podpora

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 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage01/main.py
2 average.py modul s funkcí pro výpočet průměru (implementovaný korektně) https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage01/average.py
3 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage01/test_average.py
4 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage01/run
5 test skript pro spuštění jednotkových testů i pro zjištění pokrytí kódu testy https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage01/test
       
6 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage02/main.py
7 average.py modul s funkcí pro výpočet průměru, který obsahuje chybu https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage02/average.py
8 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage02/test_average.py
9 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage02/run
10 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage02/test
       
11 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage03/main.py
12 average.py modul s funkcí pro výpočet průměru, který obsahuje chybu https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage03/average.py
13 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage03/test_average.py
14 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage03/run
15 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage03/test
       
16 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage04/main.py
17 average.py modul s funkcí pro výpočet průměru, která může vyhodit výjimku https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage04/average.py
18 test_average.py implementace jednotkových testů společně s testem pro výpočet průměru z prázdného seznamu https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage04/test_average.py
19 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage04/run
20 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage04/test
       
21 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage05/main.py
22 average.py modul s funkcí pro výpočet průměru, která může vyhodit výjimku https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage05/average.py
23 test_average.py implementace jednotkových testů společně s analýzou vzniklé výjimky https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage05/test_average.py
24 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage05/run
25 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage05/test
       
26 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage06/main.py
27 average.py modul s funkcí pro výpočet průměru, která může vyhodit výjimku https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage06/average.py
28 test_average.py implementace jednotkových testů společně s analýzou vzniklé výjimky https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage06/test_average.py
29 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage06/run
30 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage06/test
       
31 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage07/main.py
32 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage07/average.py
33 test_average.py implementace jednotkových testů používajících dekorátor @pytest.mark.parametrize https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage07/test_average.py
34 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage07/run
35 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage07/test
       
36 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage08/main.py
37 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage08/average.py
38 test_average.py implementace jednotkových testů s dekorátorem @pytest.mark.parametrize a specifikací identifikátorů testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage08/test_average.py
39 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage08/run
40 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage08/test
       
41 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage09/main.py
42 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage09/average.py
43 test_average.py implementace jednotkových testů, kde parametry testu jsou uvedeny přímo v dekorátoru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage09/test_average.py
44 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage09/run
45 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage09/test
       
46 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage10/main.py
47 average.py modul s funkcí pro výpočet průměru i špagetovým kódem (pro test výjimek) https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage10/average.py
48 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage10/test_average.py
49 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage10/run
50 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage10/test
       
51 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage11/main.py
52 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage11/average.py
53 test_average.py implementace jednotkových testů, každý test má uvedenou vlastní značku https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage11/test_average.py
54 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage11/run
55 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage11/test
       
56 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage12/main.py
57 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage12/average.py
58 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage12/test_average.py
59 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage12/run
60 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage12/test
       
56 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/main.py
57 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/average.py
57 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/average.py
58 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/test_average.py
59 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/run
60 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/test
       
61 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/main.py
57 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage13/average.py
62 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/average.py
63 test_average.py implementace jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/test_average.py
64 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/run
65 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/test

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. 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/
  5. 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/
  6. 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/
  7. 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/
  8. Validace datových struktur v Pythonu (2. část)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/
  9. Validace datových struktur v Pythonu (dokončení)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/
  10. Univerzální testovací nástroj Robot Framework
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/
  11. Univerzální testovací nástroj Robot Framework a BDD testy
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/
  12. Úvod do problematiky fuzzingu a fuzz testování
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/
  13. Ú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/
  14. 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/
  15. 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/
  16. Testování aplikací naprogramovaných v jazyce Go
    https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/
  17. 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/
  18. 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/
  19. 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/
  20. 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/
  21. 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/
  22. 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/
  23. 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/
  24. 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. pytest 5.4.2 na PyPi
    https://pypi.org/project/pytest/
  2. Awesome Python – testing
    https://github.com/vinta/awesome-python#testing
  3. pytest Plugins Compatibility
    http://plugincompat.herokuapp.com/
  4. Selenium (pro Python)
    https://pypi.org/project/selenium/
  5. Getting Started With Testing in Python
    https://realpython.com/python-testing/
  6. unittest.mock — mock object library
    https://docs.python.org/3­.5/library/unittest.mock.html
  7. mock 2.0.0
    https://pypi.python.org/pypi/mock
  8. An Introduction to Mocking in Python
    https://www.toptal.com/python/an-introduction-to-mocking-in-python
  9. Mock – Mocking and Testing Library
    http://mock.readthedocs.io/en/stable/
  10. Python Mocking 101: Fake It Before You Make It
    https://blog.fugue.co/2016–02–11-python-mocking-101.html
  11. Nauč se Python! – Testování
    http://naucse.python.cz/les­sons/intro/testing/
  12. Flexmock (dokumentace)
    https://flexmock.readthedoc­s.io/en/latest/
  13. Test Fixture (Wikipedia)
    https://en.wikipedia.org/wi­ki/Test_fixture
  14. Mock object (Wikipedia)
    https://en.wikipedia.org/wi­ki/Mock_object
  15. Extrémní programování
    https://cs.wikipedia.org/wi­ki/Extr%C3%A9mn%C3%AD_pro­gramov%C3%A1n%C3%AD
  16. Programování řízené testy
    https://cs.wikipedia.org/wi­ki/Programov%C3%A1n%C3%AD_%C5%99%­C3%ADzen%C3%A9_testy
  17. Pip (dokumentace)
    https://pip.pypa.io/en/stable/
  18. Tox
    https://tox.readthedocs.io/en/latest/
  19. pytest: helps you write better programs
    https://docs.pytest.org/en/latest/
  20. doctest — Test interactive Python examples
    https://docs.python.org/dev/li­brary/doctest.html#module-doctest
  21. unittest — Unit testing framework
    https://docs.python.org/dev/li­brary/unittest.html
  22. Python namespaces
    https://bytebaker.com/2008/07/30/pyt­hon-namespaces/
  23. Namespaces and Scopes
    https://www.python-course.eu/namespaces.php
  24. Stránka projektu Robot Framework
    https://robotframework.org/
  25. GitHub repositář Robot Frameworku
    https://github.com/robotfra­mework/robotframework
  26. Robot Framework (Wikipedia)
    https://en.wikipedia.org/wi­ki/Robot_Framework
  27. Tutoriál Robot Frameworku
    http://www.robotframeworktu­torial.com/
  28. Robot Framework Documentation
    https://robotframework.or­g/robotframework/
  29. Robot Framework Introduction
    https://blog.testproject.i­o/2016/11/22/robot-framework-introduction/
  30. robotframework 3.1.2 na PyPi
    https://pypi.org/project/ro­botframework/
  31. Robot Framework demo (GitHub)
    https://github.com/robotfra­mework/RobotDemo
  32. Robot Framework web testing demo using SeleniumLibrary
    https://github.com/robotfra­mework/WebDemo
  33. Robot Framework for Mobile Test Automation Demo
    https://www.youtube.com/wat­ch?v=06LsU08slP8
  34. Gherkin
    https://cucumber.io/docs/gherkin/
  35. Selenium
    https://selenium.dev/
  36. SeleniumLibrary
    https://robotframework.org/
  37. The Practical Test Pyramid
    https://martinfowler.com/ar­ticles/practical-test-pyramid.html
  38. Acceptance Tests and the Testing Pyramid
    http://www.blog.acceptance­testdrivendevelopment.com/ac­ceptance-tests-and-the-testing-pyramid/
  39. Tab-separated values
    https://en.wikipedia.org/wiki/Tab-separated_values
  40. A quick guide about Python implementations
    https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321
  41. radamsa
    https://gitlab.com/akihe/radamsa
  42. Fuzzing (Wikipedia)
    https://en.wikipedia.org/wiki/Fuzzing
  43. american fuzzy lop
    http://lcamtuf.coredump.cx/afl/
  44. Fuzzing: the new unit testing
    https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1
  45. Corpus for github.com/dvyukov/go-fuzz examples
    https://github.com/dvyukov/go-fuzz-corpus
  46. AFL – QuickStartGuide.txt
    https://github.com/google/AF­L/blob/master/docs/QuickStar­tGuide.txt
  47. Introduction to Fuzzing in Python with AFL
    https://alexgaynor.net/2015/a­pr/13/introduction-to-fuzzing-in-python-with-afl/
  48. Writing a Simple Fuzzer in Python
    https://jmcph4.github.io/2018/01/19/wri­ting-a-simple-fuzzer-in-python/
  49. How to Fuzz Go Code with go-fuzz (Continuously)
    https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/
  50. Golang Fuzzing: A go-fuzz Tutorial and Example
    http://networkbit.ch/golang-fuzzing/
  51. Fuzzing Python Modules
    https://stackoverflow.com/qu­estions/20749026/fuzzing-python-modules
  52. 0×3 Python Tutorial: Fuzzer
    http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/
  53. fuzzing na PyPi
    https://pypi.org/project/fuzzing/
  54. Fuzzing 0.3.2 documentation
    https://fuzzing.readthedoc­s.io/en/latest/
  55. Randomized testing for Go
    https://github.com/dvyukov/go-fuzz
  56. HTTP/2 fuzzer written in Golang
    https://github.com/c0nrad/http2fuzz
  57. 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
  58. Continuous Fuzzing Made Simple
    https://fuzzit.dev/
  59. Halt and Catch Fire
    https://en.wikipedia.org/wi­ki/Halt_and_Catch_Fire#In­tel_x86
  60. Random testing
    https://en.wikipedia.org/wi­ki/Random_testing
  61. Monkey testing
    https://en.wikipedia.org/wi­ki/Monkey_testing
  62. 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
  63. libFuzzer – a library for coverage-guided fuzz testing
    https://llvm.org/docs/LibFuzzer.html
  64. fuzzy-swagger na PyPi
    https://pypi.org/project/fuzzy-swagger/
  65. fuzzy-swagger na GitHubu
    https://github.com/namuan/fuzzy-swagger
  66. Fuzz testing tools for Python
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy#Fuzz_Testing_Tools
  67. A curated list of awesome Go frameworks, libraries and software
    https://github.com/avelino/awesome-go
  68. gofuzz: a library for populating go objects with random values
    https://github.com/google/gofuzz
  69. tavor: A generic fuzzing and delta-debugging framework
    https://github.com/zimmski/tavor
  70. hypothesis na GitHubu
    https://github.com/Hypothe­sisWorks/hypothesis
  71. Hypothesis: Test faster, fix more
    https://hypothesis.works/
  72. Hypothesis
    https://hypothesis.works/ar­ticles/intro/
  73. What is Hypothesis?
    https://hypothesis.works/articles/what-is-hypothesis/
  74. Databáze CVE
    https://www.cvedetails.com/
  75. Fuzz test Python modules with libFuzzer
    https://github.com/eerimoq/pyfuzzer
  76. Taof – The art of fuzzing
    https://sourceforge.net/pro­jects/taof/
  77. JQF + Zest: Coverage-guided semantic fuzzing for Java
    https://github.com/rohanpadhye/jqf
  78. http2fuzz
    https://github.com/c0nrad/http2fuzz
  79. Demystifying hypothesis testing with simple Python examples
    https://towardsdatascience­.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294
  80. Testování
    http://voho.eu/wiki/testovani/
  81. Unit testing (Wikipedia.en)
    https://en.wikipedia.org/wi­ki/Unit_testing
  82. Unit testing (Wikipedia.cz)
    https://cs.wikipedia.org/wi­ki/Unit_testing
  83. Unit Test vs Integration Test
    https://www.youtube.com/wat­ch?v=0GypdsJulKE
  84. TestDouble
    https://martinfowler.com/bli­ki/TestDouble.html
  85. Test Double
    http://xunitpatterns.com/Tes­t%20Double.html
  86. Test-driven development (Wikipedia)
    https://en.wikipedia.org/wiki/Test-driven_development
  87. Acceptance test–driven development
    https://en.wikipedia.org/wi­ki/Acceptance_test%E2%80%93dri­ven_development
  88. Gauge
    https://gauge.org/
  89. Gauge (software)
    https://en.wikipedia.org/wi­ki/Gauge_(software)
  90. PYPL PopularitY of Programming Language
    https://pypl.github.io/PYPL.html
  91. 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
  92. Články a zprávičky věnující se Pythonu
    https://www.root.cz/n/python/
  93. PythonTestingToolsTaxonomy
    https://wiki.python.org/mo­in/PythonTestingToolsTaxo­nomy
  94. Top 6 BEST Python Testing Frameworks [Updated 2020 List]
    https://www.softwaretestin­ghelp.com/python-testing-frameworks/
  95. pytest-print 0.1.3
    https://pypi.org/project/pytest-print/
  96. pytest fixtures: explicit, modular, scalable
    https://docs.pytest.org/en/la­test/fixture.html
  97. PyTest Tutorial: What is, Install, Fixture, Assertions
    https://www.guru99.com/pytest-tutorial.html
  98. Pytest – Fixtures
    https://www.tutorialspoin­t.com/pytest/pytest_fixtu­res.htm
  99. Marking test functions with attributes
    https://docs.pytest.org/en/la­test/mark.html

Byl pro vás článek přínosný?

Autor článku

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