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
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
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)) ... ... ...
Ú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.
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.
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 ===============================
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/average14/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 =========================
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 =========================
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:
19. Předchozí články s tématem testování (nejenom) v Pythonu
Tématem testování jsme se již na stránkách Rootu několikrát zabývali. Jedná se mj. o následující články:
- Použití Pythonu pro tvorbu testů: od jednotkových testů až po testy UI
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-od-jednotkovych-testu-az-po-testy-ui/ - Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-pouziti-tridy-mock-z-knihovny-unittest-mock/ - Použití nástroje pytest pro tvorbu jednotkových testů a benchmarků
https://www.root.cz/clanky/pouziti-nastroje-pytest-pro-tvorbu-jednotkovych-testu-a-benchmarku/ - Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-fixtures-vyjimky-parametrizace-testu/ - Behavior-driven development v Pythonu s využitím knihovny Behave
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave/ - Behavior-driven development v Pythonu s využitím knihovny Behave (druhá část)
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-druha-cast/ - Behavior-driven development v Pythonu s využitím knihovny Behave (závěrečná část)
https://www.root.cz/clanky/behavior-driven-development-v-pythonu-s-vyuzitim-knihovny-behave-zaverecna-cast/ - Validace datových struktur v Pythonu pomocí knihoven Schemagic a Schema
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-pomoci-knihoven-schemagic-a-schema/ - Validace datových struktur v Pythonu (2. část)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-2-cast/ - Validace datových struktur v Pythonu (dokončení)
https://www.root.cz/clanky/validace-datovych-struktur-v-pythonu-dokonceni/ - Univerzální testovací nástroj Robot Framework
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework/ - Univerzální testovací nástroj Robot Framework a BDD testy
https://www.root.cz/clanky/univerzalni-testovaci-nastroj-robot-framework-a-bdd-testy/ - Úvod do problematiky fuzzingu a fuzz testování
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani/ - Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
https://www.root.cz/clanky/uvod-do-problematiky-fuzzingu-a-fuzz-testovani-slozeni-vlastniho-fuzzeru/ - Knihovny a moduly usnadňující testování aplikací naprogramovaných v jazyce Clojure
https://www.root.cz/clanky/knihovny-a-moduly-usnadnujici-testovani-aplikaci-naprogramovanych-v-jazyce-clojure/ - Validace dat s využitím knihovny spec v Clojure 1.9.0
https://www.root.cz/clanky/validace-dat-s-vyuzitim-knihovny-spec-v-clojure-1–9–0/ - Testování aplikací naprogramovaných v jazyce Go
https://www.root.cz/clanky/testovani-aplikaci-naprogramovanych-v-jazyce-go/ - Knihovny určené pro tvorbu testů v programovacím jazyce Go
https://www.root.cz/clanky/knihovny-urcene-pro-tvorbu-testu-v-programovacim-jazyce-go/ - Testování aplikací psaných v Go s využitím knihoven Goblin a Frisby
https://www.root.cz/clanky/testovani-aplikaci-psanych-v-go-s-vyuzitim-knihoven-goblin-a-frisby/ - Testování Go aplikací s využitím knihovny GΩmega a frameworku Ginkgo
https://www.root.cz/clanky/testovani-go-aplikaci-s-vyuzitim-knihovny-gomega-mega-a-frameworku-ginkgo/ - Tvorba BDD testů s využitím jazyka Go a nástroje godog
https://www.root.cz/clanky/tvorba-bdd-testu-s-vyuzitim-jazyka-go-a-nastroje-godog/ - Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem
https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem/ - Použití Go pro automatizaci práce s aplikacemi s interaktivním příkazovým řádkem (dokončení)
https://www.root.cz/clanky/pouziti-go-pro-automatizaci-prace-s-aplikacemi-s-interaktivnim-prikazovym-radkem-dokonceni/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure/ - Použití jazyka Gherkin při tvorbě testovacích scénářů pro aplikace psané v Clojure (2)
https://www.root.cz/clanky/pouziti-jazyka-gherkin-pri-tvorbe-testovacich-scenaru-pro-aplikace-psane-v-nbsp-clojure-2/
20. Odkazy na Internetu
- pytest 5.4.2 na PyPi
https://pypi.org/project/pytest/ - Awesome Python – testing
https://github.com/vinta/awesome-python#testing - pytest Plugins Compatibility
http://plugincompat.herokuapp.com/ - Selenium (pro Python)
https://pypi.org/project/selenium/ - Getting Started With Testing in Python
https://realpython.com/python-testing/ - unittest.mock — mock object library
https://docs.python.org/3.5/library/unittest.mock.html - mock 2.0.0
https://pypi.python.org/pypi/mock - An Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-python - Mock – Mocking and Testing Library
http://mock.readthedocs.io/en/stable/ - Python Mocking 101: Fake It Before You Make It
https://blog.fugue.co/2016–02–11-python-mocking-101.html - Nauč se Python! – Testování
http://naucse.python.cz/lessons/intro/testing/ - Flexmock (dokumentace)
https://flexmock.readthedocs.io/en/latest/ - Test Fixture (Wikipedia)
https://en.wikipedia.org/wiki/Test_fixture - Mock object (Wikipedia)
https://en.wikipedia.org/wiki/Mock_object - Extrémní programování
https://cs.wikipedia.org/wiki/Extr%C3%A9mn%C3%AD_programov%C3%A1n%C3%AD - Programování řízené testy
https://cs.wikipedia.org/wiki/Programov%C3%A1n%C3%AD_%C5%99%C3%ADzen%C3%A9_testy - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - Tox
https://tox.readthedocs.io/en/latest/ - pytest: helps you write better programs
https://docs.pytest.org/en/latest/ - doctest — Test interactive Python examples
https://docs.python.org/dev/library/doctest.html#module-doctest - unittest — Unit testing framework
https://docs.python.org/dev/library/unittest.html - Python namespaces
https://bytebaker.com/2008/07/30/python-namespaces/ - Namespaces and Scopes
https://www.python-course.eu/namespaces.php - Stránka projektu Robot Framework
https://robotframework.org/ - GitHub repositář Robot Frameworku
https://github.com/robotframework/robotframework - Robot Framework (Wikipedia)
https://en.wikipedia.org/wiki/Robot_Framework - Tutoriál Robot Frameworku
http://www.robotframeworktutorial.com/ - Robot Framework Documentation
https://robotframework.org/robotframework/ - Robot Framework Introduction
https://blog.testproject.io/2016/11/22/robot-framework-introduction/ - robotframework 3.1.2 na PyPi
https://pypi.org/project/robotframework/ - Robot Framework demo (GitHub)
https://github.com/robotframework/RobotDemo - Robot Framework web testing demo using SeleniumLibrary
https://github.com/robotframework/WebDemo - Robot Framework for Mobile Test Automation Demo
https://www.youtube.com/watch?v=06LsU08slP8 - Gherkin
https://cucumber.io/docs/gherkin/ - Selenium
https://selenium.dev/ - SeleniumLibrary
https://robotframework.org/ - The Practical Test Pyramid
https://martinfowler.com/articles/practical-test-pyramid.html - Acceptance Tests and the Testing Pyramid
http://www.blog.acceptancetestdrivendevelopment.com/acceptance-tests-and-the-testing-pyramid/ - Tab-separated values
https://en.wikipedia.org/wiki/Tab-separated_values - A quick guide about Python implementations
https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321 - radamsa
https://gitlab.com/akihe/radamsa - Fuzzing (Wikipedia)
https://en.wikipedia.org/wiki/Fuzzing - american fuzzy lop
http://lcamtuf.coredump.cx/afl/ - Fuzzing: the new unit testing
https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1 - Corpus for github.com/dvyukov/go-fuzz examples
https://github.com/dvyukov/go-fuzz-corpus - AFL – QuickStartGuide.txt
https://github.com/google/AFL/blob/master/docs/QuickStartGuide.txt - Introduction to Fuzzing in Python with AFL
https://alexgaynor.net/2015/apr/13/introduction-to-fuzzing-in-python-with-afl/ - Writing a Simple Fuzzer in Python
https://jmcph4.github.io/2018/01/19/writing-a-simple-fuzzer-in-python/ - How to Fuzz Go Code with go-fuzz (Continuously)
https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/ - Golang Fuzzing: A go-fuzz Tutorial and Example
http://networkbit.ch/golang-fuzzing/ - Fuzzing Python Modules
https://stackoverflow.com/questions/20749026/fuzzing-python-modules - 0×3 Python Tutorial: Fuzzer
http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/ - fuzzing na PyPi
https://pypi.org/project/fuzzing/ - Fuzzing 0.3.2 documentation
https://fuzzing.readthedocs.io/en/latest/ - Randomized testing for Go
https://github.com/dvyukov/go-fuzz - HTTP/2 fuzzer written in Golang
https://github.com/c0nrad/http2fuzz - Ffuf (Fuzz Faster U Fool) – An Open Source Fast Web Fuzzing Tool
https://hacknews.co/hacking-tools/20191208/ffuf-fuzz-faster-u-fool-an-open-source-fast-web-fuzzing-tool.html - Continuous Fuzzing Made Simple
https://fuzzit.dev/ - Halt and Catch Fire
https://en.wikipedia.org/wiki/Halt_and_Catch_Fire#Intel_x86 - Random testing
https://en.wikipedia.org/wiki/Random_testing - Monkey testing
https://en.wikipedia.org/wiki/Monkey_testing - Fuzzing for Software Security Testing and Quality Assurance, Second Edition
https://books.google.at/books?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%22I+settled+on+the+term+fuzz%22&redir_esc=y&hl=de#v=onepage&q=%22I%20settled%20on%20the%20term%20fuzz%22&f=false - libFuzzer – a library for coverage-guided fuzz testing
https://llvm.org/docs/LibFuzzer.html - fuzzy-swagger na PyPi
https://pypi.org/project/fuzzy-swagger/ - fuzzy-swagger na GitHubu
https://github.com/namuan/fuzzy-swagger - Fuzz testing tools for Python
https://wiki.python.org/moin/PythonTestingToolsTaxonomy#Fuzz_Testing_Tools - A curated list of awesome Go frameworks, libraries and software
https://github.com/avelino/awesome-go - gofuzz: a library for populating go objects with random values
https://github.com/google/gofuzz - tavor: A generic fuzzing and delta-debugging framework
https://github.com/zimmski/tavor - hypothesis na GitHubu
https://github.com/HypothesisWorks/hypothesis - Hypothesis: Test faster, fix more
https://hypothesis.works/ - Hypothesis
https://hypothesis.works/articles/intro/ - What is Hypothesis?
https://hypothesis.works/articles/what-is-hypothesis/ - Databáze CVE
https://www.cvedetails.com/ - Fuzz test Python modules with libFuzzer
https://github.com/eerimoq/pyfuzzer - Taof – The art of fuzzing
https://sourceforge.net/projects/taof/ - JQF + Zest: Coverage-guided semantic fuzzing for Java
https://github.com/rohanpadhye/jqf - http2fuzz
https://github.com/c0nrad/http2fuzz - Demystifying hypothesis testing with simple Python examples
https://towardsdatascience.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294 - Testování
http://voho.eu/wiki/testovani/ - Unit testing (Wikipedia.en)
https://en.wikipedia.org/wiki/Unit_testing - Unit testing (Wikipedia.cz)
https://cs.wikipedia.org/wiki/Unit_testing - Unit Test vs Integration Test
https://www.youtube.com/watch?v=0GypdsJulKE - TestDouble
https://martinfowler.com/bliki/TestDouble.html - Test Double
http://xunitpatterns.com/Test%20Double.html - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Acceptance test–driven development
https://en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development - Gauge
https://gauge.org/ - Gauge (software)
https://en.wikipedia.org/wiki/Gauge_(software) - PYPL PopularitY of Programming Language
https://pypl.github.io/PYPL.html - Testing is Good. Pyramids are Bad. Ice Cream Cones are the Worst
https://medium.com/@fistsOfReason/testing-is-good-pyramids-are-bad-ice-cream-cones-are-the-worst-ad94b9b2f05f - Články a zprávičky věnující se Pythonu
https://www.root.cz/n/python/ - PythonTestingToolsTaxonomy
https://wiki.python.org/moin/PythonTestingToolsTaxonomy - Top 6 BEST Python Testing Frameworks [Updated 2020 List]
https://www.softwaretestinghelp.com/python-testing-frameworks/ - pytest-print 0.1.3
https://pypi.org/project/pytest-print/ - pytest fixtures: explicit, modular, scalable
https://docs.pytest.org/en/latest/fixture.html - PyTest Tutorial: What is, Install, Fixture, Assertions
https://www.guru99.com/pytest-tutorial.html - Pytest – Fixtures
https://www.tutorialspoint.com/pytest/pytest_fixtures.htm - Marking test functions with attributes
https://docs.pytest.org/en/latest/mark.html - pytest-print
https://pytest-print.readthedocs.io/en/latest/