Hlavní navigace

Nástroj pytest a jednotkové testy: životní cyklus testů, užitečné tipy a triky

21. 5. 2020
Doba čtení: 50 minut

Sdílet

 Autor: Depositphotos
Dnes se naposledy budeme zabývat použitím frameworku pytest při tvorbě a spouštění jednotkových testů. Ukážeme si mj. životní cyklus testů, některé užitečné přídavné moduly pro pytest, export do CSV i další užitečné tipy a triky.

Obsah

1. Test fixtures (zopakování z minula)

2. Výpis existujících fixtures

3. Jednotkové testy implementované formou třídy

4. Výstup produkovaný upravenými jednotkovými testy

5. Přídavný modul pytest-print

6. Ukázka výstupů z modulu pytest-print

7. Životní cyklus testů

8. Funkce setup_module a teardown_module

9. Třídní metody setup_class a teardown_class

10. Metody setup_method a teardown_method

11. Funkce setup_function a teardown_function

12. Export výsledků testů do XML

13. Export výsledků testů do formátu CSV

14. Detailní výpis zásobníkových rámců při vzniku chyby

15. Spuštění nástroje Pycodestyle přímo z testů

16. Automatický záznam chyb v repositáři na GitHubu

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. Test fixtures

V předchozím článku o tvorbě jednotkových testů s využitím frameworku pytest jsme se mj. zmínili i o existenci takzvaných test fixtures (popř. zkráceně jen fixtures), které umožňují (mj. ) připravit kontext pro spouštění jednotkových testů, generovat testovací data apod. Fixture je reprezentována funkcí s anotací @pytest.fixture:

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

Jakýkoli jednotkový test, který akceptuje parametr se stejným názvem, jako má fixture (v tomto konkrétním případě input_values) získá při svém zavolání návratovou hodnotu z funkce input_values, kterou je možné v testech použít:

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)

Minule jsme si uvedli i kompletní příklad s několika jednotkovými testy:

"""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)

Můžeme ovšem využít i existující fixtures, například cache, která si dokáže zapamatovat hodnoty mezi spuštěními testů (ve skutečnosti se v příkladu používá ještě fixture printer popsaná níže):

"""Implementace jednotkových testů."""
 
import pytest
 
 
def test_cache(printer, cache):
    """Test fixture cache."""
    counter = cache.get("foobar/counter", 0)
    printer(counter)
    counter += 1
    cache.set("foobar/counter", counter)

Příklad dvojího spuštění testů, pokaždé s jinou hodnotou uloženou do čítače a zapamatovanou v cache:

===================================================================== 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/cache
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, csv-2.0.2, cov-2.5.1
collected 1 item
 
test_cache.py::test_cache
        1
PASSED
 
====================================================================== 1 passed in 0.02s ======================================================================
 
 
 
===================================================================== 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/cache
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, csv-2.0.2, cov-2.5.1
collected 1 item
 
test_cache.py::test_cache
        2
PASSED

2. Výpis existujících fixtures

Mnoho fixtures je poskytováno jak vlastním frameworkem pytest, tak i přídavnými moduly (pluginy). Základní informace o nich je možné získat příkazem:

$ pytest --fixtures

Nejdříve se vypíšou standardní fixtures a následně fixtures nainstalované v rámci přídavných modulů:

cache
    Return a cache object that can persist state between testing sessions.
 
    cache.get(key, default)
    cache.set(key, value)
 
    Keys must be a ``/`` separated value, where the first part is usually the
    name of your plugin or application to avoid clashes with other cache users.
     
    Values can be any object handled by the json stdlib module.
 
capsys
    Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
     
    The captured output is made available via ``capsys.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``text`` objects.
 
...
...
...
---------------- fixtures defined from pytest_benchmark.plugin -----------------
benchmark
    /home/ptisnovs/.local/lib/python3.6/site-packages/pytest_benchmark/plugin.py:391: no docstring available
 
benchmark_weave
    /home/ptisnovs/.local/lib/python3.6/site-packages/pytest_benchmark/plugin.py:415: no docstring available
 
 
------------------- fixtures defined from pytest_cov.plugin --------------------
cov
    A pytest fixture to provide access to the underlying coverage object.
 
 
---------------------- fixtures defined from pytest_print ----------------------
printer
    pytest plugin to print test progress steps in verbose mode
 
 
============================ no tests ran in 0.02s =============================

3. Jednotkové testy implementované formou třídy

Jednotkové testy nemusí být tvořeny pouze jednotlivými funkcemi; můžeme namísto nich vytvořit třídy, typicky každou třídu pro jednu testovanou jednotku nebo testovanou oblast. Musí se pouze dodržet jmenné konvence, tj. třída s implementací jednotkových testů by měla začínat slovem Test a metody s testy předponou test_. Taktéž by třída s implementací jednotkových testů neměla obsahovat konstruktor (aby pytest omylem nepracovat se třídou, která čistě náhodou začíná slovem Test, ovšem nemá s jednotkovými testy nic společného).

Přepis jednotkových testů do nové podoby ve skutečnosti není vůbec složitý, protože lze stále používat test fixtures atd. Metodám s implementací testů se pochopitelně předává parametr self (pokud se nejedná o třídní metody):

"""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.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
 
 
class TestAverageFunction:
    """Jednotkové testy pro otestování funkce average z modulu average."""
 
    @pytest.mark.parametrize("values,expected", testdata)
    def test_average_basic_1(self, 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.parametrize("values,expected", testdata, ids=["1,1", "1,2", "0,1", "1,2,3", "0,10"])
    def test_average_basic_2(self, 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.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(self, 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(self):
        """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(self):
        """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"
 
    def test_average_five_values(self, 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)

4. Výstup produkovaný upravenými jednotkovými testy

Po spuštění jednotkových testů s přepínačem -v získáme výstup, který je poněkud odlišný od výstupů, které jsme prozatím viděli. Je to logické, protože plné jméno jednotlivých testů nyní musí obsahovat i jméno třídy a metody:

============================= 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/tests_in_class, inifile: pytest.ini
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 19 items
 
test_average.py::TestAverageFunction::test_average_basic_1[values0-1] PASSED   [  5%]
test_average.py::TestAverageFunction::test_average_basic_1[values1-1.5] PASSED [ 10%]
test_average.py::TestAverageFunction::test_average_basic_1[values2-0.5] PASSED [ 15%]
test_average.py::TestAverageFunction::test_average_basic_1[values3-2.0] PASSED [ 21%]
test_average.py::TestAverageFunction::test_average_basic_1[values4-0.5] FAILED [ 26%]
test_average.py::TestAverageFunction::test_average_basic_2[1,1] PASSED         [ 31%]
test_average.py::TestAverageFunction::test_average_basic_2[1,2] PASSED         [ 36%]
test_average.py::TestAverageFunction::test_average_basic_2[0,1] PASSED         [ 42%]
test_average.py::TestAverageFunction::test_average_basic_2[1,2,3] PASSED       [ 47%]
test_average.py::TestAverageFunction::test_average_basic_2[0,10] FAILED        [ 52%]
test_average.py::TestAverageFunction::test_average_basic_3[values0-1] PASSED   [ 57%]
test_average.py::TestAverageFunction::test_average_basic_3[values1-1.5] PASSED [ 63%]
test_average.py::TestAverageFunction::test_average_basic_3[values2-0.5] PASSED [ 68%]
test_average.py::TestAverageFunction::test_average_basic_3[values3-2.0] PASSED [ 73%]
test_average.py::TestAverageFunction::test_average_basic_3[values4-0.5] FAILED [ 78%]
test_average.py::TestAverageFunction::test_average_basic_3[values5-0] FAILED   [ 84%]
test_average.py::TestAverageFunction::test_average_empty_list_1 PASSED         [ 89%]
test_average.py::TestAverageFunction::test_average_empty_list_2 PASSED         [ 94%]
test_average.py::TestAverageFunction::test_average_five_values PASSED          [100%]
 
=================================== FAILURES ===================================
____________ TestAverageFunction.test_average_basic_1[values4-0.5] _____________
 
self = <test_average.TestAverageFunction object at 0x7f980b260588>
values = (0, 10), expected = 0.5
 
    @pytest.mark.parametrize("values,expected", testdata)
    def test_average_basic_1(self, 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
________________ TestAverageFunction.test_average_basic_2[0,10] ________________
 
self = <test_average.TestAverageFunction object at 0x7f980b57ce10>
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(self, 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:42: AssertionError
____________ TestAverageFunction.test_average_basic_3[values4-0.5] _____________
 
self = <test_average.TestAverageFunction object at 0x7f980aa14080>
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
            ),
            pytest.param(
                (), 0
            ),
        ],
    )
    def test_average_basic_3(self, 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:70: AssertionError
_____________ TestAverageFunction.test_average_basic_3[values5-0] ______________
 
self = <test_average.TestAverageFunction object at 0x7f980aa4b240>, values = ()
expected = 0
 
    @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(self, values, expected):
        """Otestování výpočtu průměru."""
>       result = average(values)
 
test_average.py:69:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
average.py:6: in average
    return f1(x)
average.py:11: in f1
    return f2(x)
average.py:16: in f2
    return f3(x)
average.py:21: in f3
    return f4(x)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f4(x):
        """Část špagetového kódu testovaného modulu."""
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
average.py:26: ZeroDivisionError
=========================== short test summary info ============================
FAILED test_average.py::TestAverageFunction::test_average_basic_1[values4-0.5]
FAILED test_average.py::TestAverageFunction::test_average_basic_2[0,10] - Ass...
FAILED test_average.py::TestAverageFunction::test_average_basic_3[values4-0.5]
FAILED test_average.py::TestAverageFunction::test_average_basic_3[values5-0]
========================= 4 failed, 15 passed in 0.09s =========================

5. Přídavný modul pytest-print

V dalších kapitolách budeme potřebovat zajistit tisk nějakých textových zpráv přímo v průběhu testování. Testovací framework pytest ovšem (pokud není nějakým vhodným způsobem rekonfigurován) standardní i chybový výstup zachycuje a provede tisk zpráv jen u těch testů, které zhavarují. Jedno z možných řešení tohoto problému představuje použití přídavného modulu (plugin) nazvaného příznačně pytest-print. Ten nainstalujeme snadno – stejným způsobem jako jakýkoli jiný balíček Pythonu:

$ pip3 install --user pytest-print
Collecting pytest-print
  Downloading https://files.pythonhosted.org/packages/1c/35/e9c31c1473758c4388778644cc9b0048eb1fdeb827ba4a28789e35ed4dc5/pytest_print-0.1.3-py2.py3-none-any.whl
Requirement already satisfied: pytest<6,>=3.0.0 in ./.local/lib/python3.6/site-packages (from pytest-print)
Requirement already satisfied: six<2,>=1.10.0 in ./.local/lib/python3.6/site-packages (from pytest-print)
Requirement already satisfied: attrs>=17.4.0 in /usr/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: packaging in ./.local/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: wcwidth in ./.local/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: importlib-metadata>=0.12; python_version < "3.8" in ./.local/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: py>=1.5.0 in /usr/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: pluggy<1.0,>=0.12 in ./.local/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: more-itertools>=4.0.0 in ./.local/lib/python3.6/site-packages (from pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: pyparsing>=2.0.2 in /usr/lib/python3.6/site-packages (from packaging->pytest<6,>=3.0.0->pytest-print)
Requirement already satisfied: zipp>=0.5 in ./.local/lib/python3.6/site-packages (from importlib-metadata>=0.12; python_version < "3.8"->pytest<6,>=3.0.0->pytest-print)
Installing collected packages: pytest-print
Successfully installed pytest-print-0.1.3

Tento modul vytváří nový text fixture nazvaný printer. Způsob jeho použití si ukážeme v navazující kapitole.

6. Ukázka výstupů z modulu pytest-print

V předchozí kapitole jsme si řekli, že modul pytest-printer vytváří nový text fixture nazvaný printer. Jedná se o funkci, které se předá řetězec, jenž je posléze vytištěn na standardní výstup. Ukažme si nyní způsob použití:

@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic_1(printer, values, expected):
    """Otestování výpočtu průměru."""
    printer("About to compute average from {} with expected output {}".format(values, expected))
    ...
    ...
    ...
Poznámka: na pořadí předání text fixtures nezáleží, parametry je možné libovolně prohazovat.

Úplný zdrojový text s jednotkovými testy bude vypadat následovně. Vidíme, že se jedná o zjednodušenou variantu testů, s nimiž jsme se setkali minule i v úvodních kapitolách:

"""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), 5.0),
]
 
 
@pytest.mark.parametrize("values,expected", testdata)
def test_average_basic_1(printer, values, expected):
    """Otestování výpočtu průměru."""
    printer("About to compute average from {} with expected output {}".format(values, expected))
    result = average(values)
    printer("Computed average is {}".format(result))
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

V případě, že jednotkové testy spustíme běžným způsobem, nebude žádný výstup zachycen:

$ pytest

S výstupem:

============================= 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/printer
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collected 5 items
 
test_average.py .....
 
============================== 5 passed in 0.02s ===============================

Nepomůže nám ani přepínač -v (verbose):

$ pytest -v

S výstupem:

============================= 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/printer
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 5 items
 
test_average.py::test_average_basic_1[values0-1] PASSED                   [ 20%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                 [ 40%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                 [ 60%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                 [ 80%]
test_average.py::test_average_basic_1[values4-5.0] PASSED                 [100%]
 
============================== 5 passed in 0.02s ===============================

Nutné je přidat přepínač -s nebo jeho delší podobu –capture=no, který zajistí, že se výstup zobrazí:

$ pytest -s -v

Nyní je již patrné, jak se zobrazí zprávy, které v testech vytváříme:

============================= 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/printer
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 5 items
 
test_average.py::test_average_basic_1[values0-1]
        About to compute average from (1, 1) with expected output 1
        Computed average is 1.0
PASSED
test_average.py::test_average_basic_1[values1-1.5]
        About to compute average from (1, 2) with expected output 1.5
        Computed average is 1.5
PASSED
test_average.py::test_average_basic_1[values2-0.5]
        About to compute average from (0, 1) with expected output 0.5
        Computed average is 0.5
PASSED
test_average.py::test_average_basic_1[values3-2.0]
        About to compute average from (1, 2, 3) with expected output 2.0
        Computed average is 2.0
PASSED
test_average.py::test_average_basic_1[values4-5.0]
        About to compute average from (0, 10) with expected output 5.0
        Computed average is 5.0
PASSED
 
============================== 5 passed in 0.02s ===============================

Při použití přepínače –print-relative-time se navíc před zprávami zobrazí relativní čas počítaný od spuštění testu:

$ pytest -s -v --print-relative-time

S výstupem:

============================= 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/printer
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 5 items
 
test_average.py::test_average_basic_1[values0-1]
        0.000461        About to compute average from (1, 1) with expected output 1
        0.000506        Computed average is 1.0
PASSED
test_average.py::test_average_basic_1[values1-1.5]
        0.000346        About to compute average from (1, 2) with expected output 1.5
        0.00038 Computed average is 1.5
PASSED
test_average.py::test_average_basic_1[values2-0.5]
        0.000346        About to compute average from (0, 1) with expected output 0.5
        0.00038 Computed average is 0.5
PASSED
test_average.py::test_average_basic_1[values3-2.0]
        0.000344        About to compute average from (1, 2, 3) with expected output 2.0
        0.000378        Computed average is 2.0
PASSED
test_average.py::test_average_basic_1[values4-5.0]
        0.000353        About to compute average from (0, 10) with expected output 5.0
        0.000386        Computed average is 5.0
PASSED
 
============================== 5 passed in 0.02s ===============================

7. Životní cyklus testů

Prozatím jsme jednotlivé jednotkové testy spouštěli zcela nezávisle na sobě, bez kontextu a bez nutnosti přípravy a posléze finalizace nějakých objektů. Ovšem v praxi bývá situace složitější, protože jednotkové testy jsou spouštěny v rámci nějakého kontextu (řekněme zjednodušeně připraveného prostředí), které je zapotřebí připravit, popř. nakonec zrušit. A právě pro tyto účely podporuje nástroj pytest životní cyklus testů – s využitím speciálně pojmenovaných funkcí a metod je možné zajistit kontext, a to jak v rámci celého modulu, tak i v rámci třídy s implementací testů či dokonce jen pro jedinou metodu nebo funkci. Možnosti nabízené pytestem v této oblasti jsou popsány v navazujících kapitolách.

Poznámka: v příkladech budeme používat text fixture nazvaný printer, s nímž jsme se seznámili v rámci předchozích dvou kapitol.

8. Funkce setup_module a teardown_module

Funkce nazvaná setup_module je spuštěná – pokud ovšem existuje – na začátku inicializace modulu s jednotkovým testem. Podobně funkce pojmenovaná teardown_module je spuštěna po dokončení všech jednotkových testů v tomto modulu. Oběma zmíněným funkcím je předán objekt s informacemi o modulu, který je tak možné modifikovat.

Poznámka: tyto dvě funkce akceptují vždy pouze jediný parametr – module. Není zde možné využít žádné test fixtures, což je nepochybně škoda (například by se nám hodil fixture printer).

Příklad velmi jednoduchého jednotkového testu, který obě funkce obsahuje:

"""Implementace jednotkových testů."""
 
import pytest
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    print("\nSETUP\n")
 
 
def teardown_module(module):
    """Zavoláno při finalizaci modulu s testem."""
    print("\nTEARDOWN\n")
 
 
def test_1(printer):
    """Kostra jednotkového testu."""
    printer("Test #1")
 
 
def test_2(printer):
    """Kostra jednotkového testu."""
    printer("Test #2")
 
 
testdata = [
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 4),
]
 
 
@pytest.mark.parametrize("value,expected", testdata)
def test_succ(printer, value, expected):
    """Otestování výpočtu následují hodnoty v číselné řadě."""
    printer("Test succ")
    result = value+1
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Výpis výsledků 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
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/lifecycle_module
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collected 6 items
 
test_module.py
SETUP
 
......
TEARDOWN
 
 
 
============================== 6 passed in 0.02s ===============================

Samozřejmě můžeme povolit režim verbose, který využije i funkci typu printer:

============================= 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/lifecycle_module
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 6 items
 
test_module.py::test_1
SETUP
 
 
        Test #1
PASSED
test_module.py::test_2
        Test #2
PASSED
test_module.py::test_succ[0-1]
        Test succ
PASSED
test_module.py::test_succ[1-2]
        Test succ
PASSED
test_module.py::test_succ[2-3]
        Test succ
PASSED
test_module.py::test_succ[3-4]
        Test succ
PASSED
TEARDOWN
 
 
 
============================== 6 passed in 0.02s ===============================

9. Třídní metody setup_class a teardown_class

Je možné předepsat i třídní metody se jmény setup_class a teardown_class, které jsou testovacím frameworkem pytest zavolány ve chvíli, kdy je inicializována nebo naopak finalizována třída s implementací jednotkových testů. Těmto metodám je poslán objekt představující celou třídu (tedy v idiomaticky napsaném kódu nikoli self ale cls) a opět platí, že neakceptují žádné test fixtures. Ukažme si příklad:

"""Implementace jednotkových testů."""
 
import pytest
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    print("SETUP MODULE")
 
 
def teardown_module(module):
    """Zavoláno při finalizaci modulu s testem."""
    print("TEARDOWN MODULE")
 
 
class TestClass:
    """Jednotkové testy ve třídě."""
 
    @classmethod
    def setup_class(cls):
        """Zavoláno při inicializaci třídy s testy."""
        print("SETUP CLASS")
 
    @classmethod
    def teardown_class(cls):
        """Zavoláno při finalizaci třídy s testy."""
        print("\nTEARDOWN CLASS")
 
    def test_1(self):
        """Kostra jednotkového testu."""
        print("Test #1")
 
    def test_2(self):
        """Kostra jednotkového testu."""
        print("Test #2")

Zprávy ve výsledcích jsou poněkud přeházené, protože testovací framework pytest vypisuje informace o spouštěném testu již při analýze skriptů s jednotkovými testy:

============================= 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/lifecycle_class
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 2 items
 
test_class.py::TestClass::test_1 SETUP MODULE
SETUP CLASS
Test #1
PASSED
test_class.py::TestClass::test_2 Test #2
PASSED
TEARDOWN CLASS
TEARDOWN MODULE
 
 
============================== 2 passed in 0.01s ===============================
Poznámka: z předchozího výpisu je zřejmé, proč je tisk jakýchkoli zpráv v průběhu testování poměrně problematické a spíše matoucí.

10. Metody setup_method a teardown_method

Třetí skupina struktur umožňujících ovlivnění kontextu, v němž se jednotkové testy spouští, je vázána k metodám s implementací jednotkových testů. Před každou takovou metodou je možné spustit jinou metodu nazvanou setup_method a po ukončení pak metodu teardown_method. Ukažme si nyní nepatrně upravený předchozí demonstrační příklad, v němž jsou tyto metody definovány (jedná se o běžné metody objektu):

"""Implementace jednotkových testů."""
 
import pytest
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    print("SETUP MODULE")
 
 
def teardown_module(module):
    """Zavoláno při finalizaci modulu s testem."""
    print("TEARDOWN MODULE")
 
 
class TestClass:
    """Jednotkové testy ve třídě."""
 
    @classmethod
    def setup_class(cls):
        """Zavoláno při inicializaci třídy s testy."""
        print("SETUP CLASS")
 
    @classmethod
    def teardown_class(cls):
        """Zavoláno při finalizaci třídy s testy."""
        print("\nTEARDOWN CLASS")
 
    def setup_method(cls):
        """Zavoláno před každou metodou s jednotkovými testy."""
        print("SETUP METHOD")
 
    def teardown_method(cls):
        """Zavoláno po každé metodě s jednotkovými testy."""
        print("\nTEARDOWN METHOD")
 
    def test_1(self):
        """Kostra jednotkového testu."""
        print("Test #1")
 
    def test_2(self):
        """Kostra jednotkového testu."""
        print("Test #2")
 
    def test_3(self):
        """Kostra jednotkového testu."""
        print("Test #3")

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/lifecycle_method
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 3 items
 
test_method.py::TestClass::test_1 SETUP MODULE
SETUP CLASS
SETUP METHOD
Test #1
PASSED
TEARDOWN METHOD
 
test_method.py::TestClass::test_2 SETUP METHOD
Test #2
PASSED
TEARDOWN METHOD
 
test_method.py::TestClass::test_3 SETUP METHOD
Test #3
PASSED
TEARDOWN METHOD
 
TEARDOWN CLASS
TEARDOWN MODULE
 
 
============================== 3 passed in 0.01s ===============================

11. Funkce setup_function a teardown_function

Funkce nazvaná setup_function, která akceptuje jediný parametr s informacemi o testovací funkci, je zavolána před každou funkcí s implementací jednotkového testu. Podobně nazvaná funkce teardown_function je – jak již ostatně správně očekáváte – zavolána po každém jednotkovém testu. Opět platí, že tyto speciálně pojmenované funkce nemohou akceptovat žádný další fixture. Podívejme se na jednoduchý (umělý) příklad s jednotkovými testy:

"""Implementace jednotkových testů."""
 
import pytest
 
 
def setup_module(module):
    """Zavoláno při inicializaci modulu s testem."""
    print("\nSETUP MODULE\n")
 
 
def teardown_module(module):
    """Zavoláno při finalizaci modulu s testem."""
    print("\nTEARDOWN MODULE")
 
 
def setup_function(function):
    """Zavoláno při inicializaci funkce s testem."""
    print("\nSETUP FUNCTION")
 
 
def teardown_function(function):
    """Zavoláno při finalizaci funkce s testem."""
    print("\nTEARDOWN FUNCTION")
 
 
def test_1(printer):
    """Kostra jednotkového testu."""
    printer("Test #1")
 
 
def test_2(printer):
    """Kostra jednotkového testu."""
    printer("Test #2")
 
 
testdata = [
        (0, 1),
        (1, 2),
        (2, 3),
        (3, 4),
]
 
 
@pytest.mark.parametrize("value,expected", testdata)
def test_succ(printer, value, expected):
    """Otestování výpočtu následují hodnoty v číselné řadě."""
    printer("Test succ")
    result = value+1
    assert result == expected, "Očekávaná hodnota {}, vráceno {}".format(expected, result)

Po spuštění testů s přepínači -v a -s dostaneme následující výsledky, které ukazují, kdy přesně (a kolikrát) se volají funkce setup_module, setup_function, teardown_function a teardown_module:

============================= 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/lifecycle_function
plugins: print-0.1.3, voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 6 items
 
test_module.py::test_1
SETUP MODULE
 
 
SETUP FUNCTION
 
        Test #1
PASSED
TEARDOWN FUNCTION
 
test_module.py::test_2
SETUP FUNCTION
 
        Test #2
PASSED
TEARDOWN FUNCTION
 
test_module.py::test_succ[0-1]
SETUP FUNCTION
 
        Test succ
PASSED
TEARDOWN FUNCTION
 
test_module.py::test_succ[1-2]
SETUP FUNCTION
 
        Test succ
PASSED
TEARDOWN FUNCTION
 
test_module.py::test_succ[2-3]
SETUP FUNCTION
 
        Test succ
PASSED
TEARDOWN FUNCTION
 
test_module.py::test_succ[3-4]
SETUP FUNCTION
 
        Test succ
PASSED
TEARDOWN FUNCTION
 
TEARDOWN MODULE
 
 
============================== 6 passed in 0.02s ===============================

12. Export výsledků testů do XML

V případě, že se jednotkové testy spouští například v prostředí CI, je nutné jejich výsledky nějakým způsobem automaticky zpracovat. Pro tento účel se ovšem příliš nehodí použití textového formátu, s nímž jsme se seznámili minule i v předchozích kapitolách. Namísto toho se typicky používá formát XML používaný například nástrojem JUnit pro Javu. I tento formát je frameworkem pytest podporován, postačuje pouze zadat formát i se jménem souboru, který se má vygenerovat:

$ pytest -v --junitxml="junit.xml"

V některých případech je striktně vyžadováno jméno souboru „results.xml“:

$ pytest -v --junitxml="results.xml"

Neformátovaný výstup vypadá následovně: https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/result.xml.

Po naformátování externím nástrojem:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
   <testsuite errors="0" failures="4" hostname="localhost.localdomain" name="pytest" skipped="0" tests="19" time="0.136" timestamp="2020-05-19T20:52:52.067423">
      <testcase classname="test_average" file="test_average.py" line="23" name="test_average_basic_1[values0-1]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="23" name="test_average_basic_1[values1-1.5]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="23" name="test_average_basic_1[values2-0.5]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="23" name="test_average_basic_1[values3-2.0]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="23" name="test_average_basic_1[values4-0.5]" time="0.003">
         <failure message="AssertionError: Očekávaná hodnota 0.5, vráceno 5.0 assert 5.0 == 0.5   +5.0   -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:29: AssertionError</failure>
      </testcase>
      <testcase classname="test_average" file="test_average.py" line="31" name="test_average_basic_2[1,1]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="31" name="test_average_basic_2[1,2]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="31" name="test_average_basic_2[0,1]" time="0.002" />
      <testcase classname="test_average" file="test_average.py" line="31" name="test_average_basic_2[1,2,3]" time="0.002" />
      <testcase classname="test_average" file="test_average.py" line="31" name="test_average_basic_2[0,10]" time="0.002">
         <failure message="AssertionError: Očekávaná hodnota 0.5, vráceno 5.0 assert 5.0 == 0.5   +5.0   -0.5">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:37: AssertionError</failure>
      </testcase>
      <testcase classname="test_average" file="test_average.py" line="39" name="test_average_basic_3[values0-1]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="39" name="test_average_basic_3[values1-1.5]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="39" name="test_average_basic_3[values2-0.5]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="39" name="test_average_basic_3[values3-2.0]" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="39" name="test_average_basic_3[values4-0.5]" time="0.001">
         <failure message="AssertionError: Očekávaná hodnota 0.5, vráceno 5.0 assert 5.0 == 0.5   +5.0   -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:67: AssertionError</failure>
      </testcase>
      <testcase classname="test_average" file="test_average.py" line="39" name="test_average_basic_3[values5-0]" time="0.001">
         <failure message="ZeroDivisionError: float division by zero">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:66:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
average.py:6: in average
    return f1(x)
average.py:11: in f1
    return f2(x)
average.py:16: in f2
    return f3(x)
average.py:21: in f3
    return f4(x)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f4(x):
        """Část špagetového kódu testovaného modulu."""
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
average.py:26: ZeroDivisionError</failure>
      </testcase>
      <testcase classname="test_average" file="test_average.py" line="69" name="test_average_empty_list_1" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="76" name="test_average_empty_list_2" time="0.001" />
      <testcase classname="test_average" file="test_average.py" line="98" name="test_average_five_values" time="0.001" />
   </testsuite>
</testsuites>

13. Export výsledků testů do formátu CSV

Výsledky testů je možné exportovat i do formátu CSV, ovšem pro tento účel je nejdřív nutné nainstalovat balíček nazvaný pytest-csv:

$ pip3 install --user pytest-csv
 
Collecting pytest-csv
  Downloading https://files.pythonhosted.org/packages/17/1f/74cc8ae9d0927ffe8bf28637868a5103b6a0d686ab046108aadc752f46a8/pytest_csv-2.0.2-py2.py3-none-any.whl
Requirement already satisfied: pytest>=4.4 in ./.local/lib/python3.6/site-packages (from pytest-csv)
Requirement already satisfied: six>=1.0.0 in ./.local/lib/python3.6/site-packages (from pytest-csv)
Requirement already satisfied: wcwidth in ./.local/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: pluggy<1.0,>=0.12 in ./.local/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: py>=1.5.0 in /usr/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: packaging in ./.local/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: more-itertools>=4.0.0 in ./.local/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: importlib-metadata>=0.12; python_version < "3.8" in ./.local/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: attrs>=17.4.0 in /usr/lib/python3.6/site-packages (from pytest>=4.4->pytest-csv)
Requirement already satisfied: pyparsing>=2.0.2 in /usr/lib/python3.6/site-packages (from packaging->pytest>=4.4->pytest-csv)
Requirement already satisfied: zipp>=0.5 in ./.local/lib/python3.6/site-packages (from importlib-metadata>=0.12; python_version < "3.8"->pytest>=4.4->pytest-csv)
Installing collected packages: pytest-csv
Successfully installed pytest-csv-2.0.2

Samotný výstup do CSV pak zařídí příkaz:

$ pytest --csv tests.csv

Takto získané výsledky je možné dále zpracovat, typicky v tabulkových procesorech:

Obrázek 1: Výsledek jednotkových testů zobrazený v tabulkovém procesoru.

14. Detailní výpis zásobníkových rámců při vzniku chyby

Ve druhé části dnešního článku si uvedeme různé více či méně praktické triky nabízené nástrojem pytest, které se mohou hodit v praxi. Prvním trikem je řízení způsobu zobrazení výpisu zásobníkových rámců ve chvíli, kdy v testovaném kódu vznikne nějaká chyba. Výpis zásobníkových rámců je možné zcela zakázat, a to následujícím způsobem:

$ pytest -v --tb=no

Výstup bude v tomto případě vypadat takto:

============================= 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/average14, inifile: pytest.ini
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 19 items
 
test_average.py::test_average_basic_1[values0-1] PASSED                  [  5%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                [ 10%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                [ 15%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                [ 21%]
test_average.py::test_average_basic_1[values4-0.5] FAILED                [ 26%]
test_average.py::test_average_basic_2[1,1] PASSED                        [ 31%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 36%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 42%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 47%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 52%]
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 57%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 63%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 68%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 73%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 78%]
test_average.py::test_average_basic_3[values5-0] FAILED                  [ 84%]
test_average.py::test_average_empty_list_1 PASSED                        [ 89%]
test_average.py::test_average_empty_list_2 PASSED                        [ 94%]
test_average.py::test_average_five_values PASSED                         [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, 15 passed in 0.09s =========================
Poznámka: můžeme vidět, že se zobrazily pouze základní informace o tom, které testy prošly bez chyby a které naopak s chybou. Žádné další informace nejsou k dispozici.

Přepínačem –showlocals zajistíme zobrazení podrobnějších informací o chybě, zejména hodnoty lokálních proměnných atd.:

E       AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
E       assert 5.0 == 0.5
E         +5.0
E         -0.5
 
expected   = 0.5
result     = 5.0
values     = (0, 10)

Opět si to ukažme v praxi:

$ pytest -v --showlocals

S výstupem:

============================= 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/average14, inifile: pytest.ini
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 19 items
 
test_average.py::test_average_basic_1[values0-1] PASSED                  [  5%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                [ 10%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                [ 15%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                [ 21%]
test_average.py::test_average_basic_1[values4-0.5] FAILED                [ 26%]
test_average.py::test_average_basic_2[1,1] PASSED                        [ 31%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 36%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 42%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 47%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 52%]
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 57%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 63%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 68%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 73%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 78%]
test_average.py::test_average_basic_3[values5-0] FAILED                  [ 84%]
test_average.py::test_average_empty_list_1 PASSED                        [ 89%]
test_average.py::test_average_empty_list_2 PASSED                        [ 94%]
test_average.py::test_average_five_values PASSED                         [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
 
expected   = 0.5
result     = 5.0
values     = (0, 10)
 
test_average.py:29: 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
 
expected   = 0.5
result     = 5.0
values     = (0, 10)
 
test_average.py:37: 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
 
expected   = 0.5
result     = 5.0
values     = (0, 10)
 
test_average.py:67: 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)
 
expected   = 0
values     = ()
 
test_average.py:66:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
average.py:6: in average
    return f1(x)
        x          = ()
average.py:11: in f1
    return f2(x)
        x          = ()
average.py:16: in f2
    return f3(x)
        x          = ()
average.py:21: in f3
    return f4(x)
        x          = ()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f4(x):
        """Část špagetového kódu testovaného modulu."""
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
x          = ()
 
average.py:26: 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, 15 passed in 0.09s =========================

Existuje však ještě jeden způsob zobrazení nazvaný „long“, který se povoluje takto:

$ pytest -v --tb=long

Ve výsledku získaném po spuštění jednotkových testů se nyní zobrazí výpis získaný průchodem zásobníkovými rámci, zde konkrétně celé pořadí volaných funkcí (to se týká zejména části označené komentáři „špagetový kód“):

============================= 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/average14, inifile: pytest.ini
plugins: voluptuous-1.0.2, benchmark-3.2.3, cov-2.5.1
collecting ... collected 19 items
 
test_average.py::test_average_basic_1[values0-1] PASSED                  [  5%]
test_average.py::test_average_basic_1[values1-1.5] PASSED                [ 10%]
test_average.py::test_average_basic_1[values2-0.5] PASSED                [ 15%]
test_average.py::test_average_basic_1[values3-2.0] PASSED                [ 21%]
test_average.py::test_average_basic_1[values4-0.5] FAILED                [ 26%]
test_average.py::test_average_basic_2[1,1] PASSED                        [ 31%]
test_average.py::test_average_basic_2[1,2] PASSED                        [ 36%]
test_average.py::test_average_basic_2[0,1] PASSED                        [ 42%]
test_average.py::test_average_basic_2[1,2,3] PASSED                      [ 47%]
test_average.py::test_average_basic_2[0,10] FAILED                       [ 52%]
test_average.py::test_average_basic_3[values0-1] PASSED                  [ 57%]
test_average.py::test_average_basic_3[values1-1.5] PASSED                [ 63%]
test_average.py::test_average_basic_3[values2-0.5] PASSED                [ 68%]
test_average.py::test_average_basic_3[values3-2.0] PASSED                [ 73%]
test_average.py::test_average_basic_3[values4-0.5] FAILED                [ 78%]
test_average.py::test_average_basic_3[values5-0] FAILED                  [ 84%]
test_average.py::test_average_empty_list_1 PASSED                        [ 89%]
test_average.py::test_average_empty_list_2 PASSED                        [ 94%]
test_average.py::test_average_five_values PASSED                         [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:29: 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:37: 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:67: 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:66:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def average(x):
        """Výpočet průměru ze seznamu hodnot předaných v parametru x."""
>       return f1(x)
 
average.py:6:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f1(x):
        """Část špagetového kódu testovaného modulu."""
>       return f2(x)
 
average.py:11:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()

    def f2(x):
        """Část špagetového kódu testovaného modulu."""
>       return f3(x)
 
average.py:16:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()

    def f3(x):
        """Část špagetového kódu testovaného modulu."""
>       return f4(x)
 
average.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 
x = ()
 
    def f4(x):
        """Část špagetového kódu testovaného modulu."""
>       return sum(x)/float(len(x))
E       ZeroDivisionError: float division by zero
 
average.py:26: 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, 15 passed in 0.09s =========================
Poznámka: předchozí volby ovlivňují i výstup do XML i dalších podporovaných výstupních formátů.

15. Spuštění nástroje Pycodestyle přímo z testů

Často se setkáme s použitím nástrojů typu pylint, pycodestyle či black pro kontrolu formátování zdrojového kódu, použití idiomatických konstrukcí apod. Tyto nástroje se typicky spouští samostatně (ideálně v rámci commitu), ovšem pokud z nějakého důvodu budete potřebovat jejich spuštění přímo v rámci testů, je to možné. Následující skript projde všemi soubory s koncovkou „.py“ umístěných v aktuálním adresáři i jeho podadresářích a pro každý takový soubor spustí nástroj pydocstyle. Na konci se vyhodnotí počet souborů obsahujících chyby či jiné nedostatky:

"""Simple checker of all Python sources in the given directory (usually repository)."""
 
from pathlib import Path
from sys import exit
import pycodestyle
 
 
def main():
    files = list(Path(".").rglob("*.py"))
 
    style = pycodestyle.StyleGuide(quiet=False, config_file='setup.cfg')
    result = style.check_files(files)
    print("Total errors:", result.total_errors)
    if result.total_errors > 0:
        exit(1)
 
 
if __name__ == "__main__":
    main()

Způsob detekce a výpisu problémů tímto skriptem:

issue.py:15:1: E302 expected 2 blank lines, found 1
issue.py:15:101: E501 line too long (147 > 100 characters)
issue.py:19:1: W293 blank line contains whitespace
issue.py:25:1: W293 blank line contains whitespace
issue.py:42:1: E305 expected 2 blank lines after class or function definition, found 1
issue.py:47:101: E501 line too long (212 > 100 characters)
pytest/average14/test_average.py:102:101: E501 line too long (104 > 100 characters)
unittest_mock/mock-test3/test.py:42:101: E501 line too long (102 > 100 characters)
unittest_mock/mock-test2/test.py:38:101: E501 line too long (102 > 100 characters)
unittest_mock/mock-testB/main.py:8:16: E231 missing whitespace after ','
unittest_mock/mock-testB/main.py:9:16: E231 missing whitespace after ','
unittest_mock/mock-testB/main.py:10:16: E231 missing whitespace after ','
unittest_mock/mock-testC/module2.py:4:1: E302 expected 2 blank lines, found 1
unittest_mock/mock-testC/module2.py:7:1: E302 expected 2 blank lines, found 1
Total errors: 14

Převod na jednotkový test (resp. kód, který dodržuje konvence jednotkového testu) je snadný:

"""Implementace jednotkových testů."""
 
from pathlib import Path
from sys import exit
import pycodestyle
import pytest
 
from average import average
 
 
def test_code_style():
    files = list(Path(".").rglob("*.py"))
 
    style = pycodestyle.StyleGuide(quiet=False, config_file='setup.cfg')
    result = style.check_files(files)
    print("Total errors:", result.total_errors)
    assert result.total_errors == 0, "Detected {} code style problems".format(result.total_errors)

Chybný formát zdrojových kódů se stane součástí výsledku běhu jednotkových testů:

FAILED test_average.py::test_average_basic_1[values4-0.5] - AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
FAILED test_average.py::test_average_basic_2[0,10] - AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
FAILED test_average.py::test_average_basic_3[values4-0.5] - AssertionError: Očekávaná hodnota 0.5, vráceno 5.0
FAILED test_average.py::test_average_basic_3[values5-0] - ZeroDivisionError: float division by zero
FAILED test_code_style.py::test_code_style - AssertionError: Detected 6 code style problems

16. Automatický záznam chyb v repositáři na GitHubu

U některých projektů může být výhodné zaznamenat nalezené chyby přímo ve formě issue(s) v repositáři s projektem. Samozřejmě není nutné tyto chyby zapisovat ručně (přes tlačítko „New Issue“), ale můžete použít následující skript (určený pouze pro GitHub), jemuž je nutné přes parametry příkazové řádky předat skupinu, repositář (z těchto dvou údajů se složí cesta k repositáři), token uživatele (získaný opět přes web UI, ten uchovejte v tajnosti), titulek issue i vlastní text s popisem (body), který může v případě potřeby obsahovat značky jazyka Markdown. Tento skript je možné spouštět v návaznosti na výsledky nástroje pytest, ovšem existuje omezení na počet požadavků posílaných přes REST API (5000 za hodinu pro jednoho uživatele, což by mělo být pro tyto účely více než dostatečné):

"""Create an issue on github.com using the given parameters."""
 
import os
import sys
import requests
import json
 
from datetime import datetime
from argparse import ArgumentParser
 
 
def current_time_formatted():
    """GitHub API accepts timestamp in following format: '2020-03-10T16:00:00Z'."""
    return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
 
 
def make_github_issue(title, body=None, created_at=None, closed_at=None, updated_at=None,
                      assignee=None, milestone=None, closed=None, labels=None, token=None,
                      organization=None, repository=None):
    """Create an issue on github.com using the given parameters."""
    # Url to create issues via POST
    url = 'https://api.github.com/repos/%s/%s/import/issues' % (organization, repository)
 
    # Headers
    headers = {
        "Authorization": "token %s" % token,
        "Accept": "application/vnd.github.golden-comet-preview+json"
    }
 
    # Create our issue
    data = {'issue': {'title': title,
                      'body': body,
                      'created_at': created_at,
                      'assignee': assignee}}
 
    payload = json.dumps(data)
 
    # Add the issue to our repository
    response = requests.request("POST", url, data=payload, headers=headers)
    if response.status_code == 202:
        print('Successfully created Issue "%s"' % title)
    else:
        print('Could not create Issue "%s"' % title)
        print('Response:', response.content)
 
 
def cli_arguments():
    """Retrieve all CLI arguments."""
    parser = ArgumentParser()
 
    # Authentication for user filing issue (must have read/write access to
    # repository to add issue to)
    parser.add_argument("-t", "--token", dest="token", help="authentication token",
                        action="store", default=None, type=str, required=True)
 
    # The repository to add this issue to
    parser.add_argument("-o", "--organization", dest="organization",
                        help="organization or repository owner",
                        action="store", default=None, type=str, required=True)
    parser.add_argument("-r", "--repository", dest="repository", help="repository name",
                        action="store", default=None, type=str, required=True)
 
    # Issue-related options
    parser.add_argument("-i", "--title", dest="title", help="issue title",
                        action="store", default=None, type=str, required=True)
 
    parser.add_argument("-b", "--body", dest="body", help="body (text) of an issue",
                        action="store", default=None, type=str, required=True)
 
    parser.add_argument("-a", "--assignee", dest="assignee", help="default assignee",
                        action="store", default=None, type=str, required=True)
 
    # Other options
    parser.add_argument("-v", "--verbose", dest="verbose", help="make operations verbose",
                        action="store_true", default=None)
 
    return parser.parse_args()
 
 
def main():
    """Entry point to this script."""
    timestamp = current_time_formatted()
    args = cli_arguments()
    make_github_issue(args.title, body=args.body, created_at=timestamp, assignee=args.assignee,
                      organization=args.organization, repository=args.repository, token=args.token)
 
 
if __name__ == "__main__":
    main()

Příklad použití je vypsán po zadání přepínače –help:

usage: issue.py [-h] -t TOKEN -o ORGANIZATION -r REPOSITORY -i TITLE -b BODY
                -a ASSIGNEE [-v]
 
optional arguments:
  -h, --help            show this help message and exit
  -t TOKEN, --token TOKEN
                        authentication token
  -o ORGANIZATION, --organization ORGANIZATION
                        organization or repository owner
  -r REPOSITORY, --repository REPOSITORY
                        repository name
  -i TITLE, --title TITLE
                        issue title
  -b BODY, --body BODY  body (text) of an issue
  -a ASSIGNEE, --assignee ASSIGNEE
                        default assignee
  -v, --verbose         make operations verbose

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 již začneme zabývat dalšími typy testů v testovací pyramidě. Bude se jednat o testy komponent a taktéž o integrační testy.

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 issue.py automatický záznam chyb v repositáři na GitHubu https://github.com/tisnik/testing-in-python/blob/master/ci/issue.py
2 run_pycodestyle.py skript pro spuštění nástroje pycodestyle a výpis všech nalezených chyb https://github.com/tisnik/testing-in-python/blob/master/run_pycodestyle.py
       
3 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/prin­ter/main.py
4 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/prin­ter/average.py
5 test_average.py implementace jednotkových testů používajících test fixture printer https://github.com/tisnik/testing-in-python/blob/master/pytest/prin­ter/test_average.py
6 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/printer/run
7 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/printer/test
       
8 test_module.py funkce setup_module a teardown_module https://github.com/tisnik/testing-in-python/blob/master/pytest/li­fecycle_module/test_module­.py
9 test_class.py třídní metody setup_class a teardown_class https://github.com/tisnik/testing-in-python/blob/master/pytest/li­fecycle_class/test_class.py
10 test_method.py metody setup_method a teardown_method https://github.com/tisnik/testing-in-python/blob/master/pytest/li­fecycle_method/test_method­.py
11 test_function.py funkce setup_function a teardown_function https://github.com/tisnik/testing-in-python/blob/master/pytest/li­fecycle_function/test_fun­ction.py
       
12 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/main.py
13 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
14 test_average.py implementace jednotkových testů používající mj. i test fixture https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/test_average.py
15 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/run
16 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/a­verage14/test
       
17 main.py vstupní bod do testované aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/tes­ts_in_class/main.py
18 average.py modul s funkcí pro výpočet průměru https://github.com/tisnik/testing-in-python/blob/master/pytest/tes­ts_in_class/average.py
19 test_average.py implementace jednotkových testů založených na použití třídy namísto „pouhých“ funkcí https://github.com/tisnik/testing-in-python/blob/master/pytest/tes­ts_in_class/test_average.py
20 run skript pro spuštění aplikace https://github.com/tisnik/testing-in-python/blob/master/pytest/tes­ts_in_class/run
21 test skript pro spuštění jednotkových testů https://github.com/tisnik/testing-in-python/blob/master/pytest/tes­ts_in_class/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. 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. 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/
  6. 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/
  7. 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/
  8. 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/
  9. Validace datových struktur v Pythonu (2. část)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/
  10. Validace datových struktur v Pythonu (dokončení)
    https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/
  11. Univerzální testovací nástroj Robot Framework
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/
  12. Univerzální testovací nástroj Robot Framework a BDD testy
    https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/
  13. Úvod do problematiky fuzzingu a fuzz testování
    https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/
  14. Ú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/
  15. 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/
  16. 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/
  17. Testování aplikací naprogramovaných v jazyce Go
    https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/
  18. 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/
  19. 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/
  20. 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/
  21. 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/
  22. 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/
  23. 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/
  24. 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/
  25. 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
  100. pytest-print
    https://pytest-print.readthedocs.io/en/latest/

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.