Obsah
1. Testování webových aplikací s REST API z Pythonu (3)
2. Poslání požadavku s využitím HTTP metody GET
3. Otestování, zda byla odpověď poslána s očekávaným HTTP kódem
4. Příprava funkcí pro knihovnu určenou pro testování REST API
5. Kontrola hlavičky nebo hlaviček vrácených serverem v odpovědi
6. Samostatná funkce pro otestování typu odpovědi
7. Test, zda server poslal rastrové obrázky či podobná data
9. Zpracování a otestování odpovědi serveru ve formátu JSON
10. Výběr atributů s využitím DSL
11. Otestování REST API endpointu dostupného přes HTTP metodu POST
12. Zaslání dat na server s využitím metody POST
13. Předání dat serveru v těle HTTP požadavku typu POST
14. Předání cookies v požadavku a test dat vrácených v odpovědi serveru
15. Správa sezení s využitím cookies
16. Kontrola složitějších datových struktur vracených serverem
17. Zpracování XML a HTML v Pythonu s využitím knihovny lxml
18. Repositář s demonstračními příklady
19. Předchozí články s tématem testování (nejenom) v Pythonu
1. Testování webových aplikací s REST API z Pythonu (3)
Na předchozí dva články [1] [2] dnes navážeme, protože si ukážeme, jakým způsobem je možné využít knihovnu Requests společně s frameworkem Pytest, jehož základní možnosti jsme si již taktéž popsali v [3] [4] a [5]. Sice by se mohlo zdát, že Pytest je určen pouze pro psaní jednotkových testů (což je skutečně jeho primárním účelem), ovšem ve skutečnosti nám nikdo nebrání v tom použít tento velmi užitečný nástroj i pro testy integrační, popř. konkrétně pro vytváření testů REST API.
Teoreticky si v mnoha případech nasazení REST API testů skutečně vystačíme pouze s dvojicí Requests+Pytest, ovšem dále uvidíme, že může být velmi výhodné začít používat i další nástroje, které například umožňují automatické či poloautomatické vytváření testů na základě specifikace OpenAPI, resp. Swaggeru. Toto téma je velmi zajímavé především u těch služeb, které mají rozsáhlé REST API, tj. obsahují mnoho koncových bodů, každý s několika HTTP metodami, mnoha parametry atd.
V závěru článku se ve stručnosti zmíníme o tom, jak je možné testovat složitější datové struktury vracené v REST API odpovědích, popř. jak lze testovat XML, popř. přímo HTML stránky (či jejich fragmenty), které jsou serverem vraceny v HTTP odpovědích.
2. Poslání požadavku s využitím HTTP metody GET
Začněme velmi jednoduchým demonstračním příkladem, v němž pouze pošleme HTTP požadavek (request) na server s REST API rozhraním a zkontrolujeme a poté provedeme „dummy“ test na existenci objektu typu response (ten se ve skutečnosti vrátí za všech okolností, protože v opačném případě by nastala výjimka, která by byla vyhozena a následně zachycena samotným Pytestem ještě předtím, než by se došlo k příkazu assert). V tomto demonstračním příkladu se tedy neprovádí žádné reálné testy, pouze si ukazujeme, jak vypadá typická funkce (nebo i metoda) s implementací REST API testů:
- Její jméno začíná prefixem „test_“
- Uvnitř funkce/metody se typicky nachází sekvence příkazů (řídicích konstrukcí) assert.
- Alternativně je možné namísto příkazu assert využít pomocné metody z testovacího frameworku unittest, které jsou vypsány zde.
Úplný zdrojový kód tohoto demonstračního příkladu naleznete na adrese https://github.com/tisnik/testing-in-python/blob/master/requests/tests/01_basic_usage_test.py a vypadá následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) assert response is not None def test_get_method_for_missing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/neexistuje" # poslani HTTP dotazu typu GET response = requests.get(URL) assert response is not None
Vzhledem k tomu, že se jedná o korektně napsané testovací funkce spustitelné nástrojem Pytest, můžeme si chování snadno otestovat:
$ pytest -v 01_basic_usage_test.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 2 items 01_basic_usage_test.py::test_get_method_for_existing_endpoint PASSED [ 50%] 01_basic_usage_test.py::test_get_method_for_missing_endpoint PASSED [100%] ============================== 2 passed in 0.71s ===============================
3. Otestování, zda byla odpověď poslána s očekávaným HTTP kódem
Z předchozích článků již víme, že stavový kód odpovědi serveru je reprezentován celým číslem, přičemž z první číslice (stovky) lze odvodit základní vlastnost stavu (úspěch, chyba, přesměrování…):
Skupina stavových kódů | Význam |
---|---|
1×x | informační, potvrzení atd. (ovšem požadavek se prozatím nevykonal) |
2×x | úspěšné vyřízení požadavku, popř. jeho akceptace serverem (202) |
3×x | přesměrování požadavku, informace o tom, že se objekt nezměnil atd. |
4×x | různé typy chyb typicky zaviněných klientem (bohužel nejrozsáhlejší skupina) |
5×x | různé chyby na serveru |
Pokud budeme dotaz posílat s využitím knihovny Requests, je možné ke stavovému kódu přistupovat přes objekt typu Response, představujícího odpověď serveru. Číselný kód stavu uložen v atributu pojmenovaném status_code. Kromě toho existuje ještě atribut nazvaný ok, který obsahuje pravdivostní hodnotu True v případě, že je číselný kód stavu menší než 400 a hodnotu False v opačném případě. Obě tyto hodnoty pochopitelně můžeme snadno otestovat:
assert response.ok assert response.status_code == 200
Tento typ testů je ukázán ve druhém demonstračním příkladu, který zjistí, zda je možný přístup na existující URL i na neexistující koncový bod. Úplný zdrojový kód tohoto demonstračního příkladu lze nalézt na adrese https://github.com/tisnik/testing-in-python/blob/master/requests/tests/02_check_status_test.py a vypadá následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) assert response is not None assert response.ok assert response.status_code == 200 def test_get_method_for_missing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/neexistuje" # poslani HTTP dotazu typu GET response = requests.get(URL) assert response is not None assert response.ok assert response.status_code == 200
Po spuštění těchto testů by mělo dojít k detekci chyby, a to konkrétně u druhého testu, který se snaží přistoupit na neexistující koncový bod REST API serveru:
$ pytest -v 02_check_status_test.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 2 items 02_check_status_test.py::test_get_method_for_existing_endpoint PASSED [ 50%] 02_check_status_test.py::test_get_method_for_missing_endpoint FAILED [100%] =================================== FAILURES =================================== _____________________ test_get_method_for_missing_endpoint _____________________ def test_get_method_for_missing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/neexistuje" # poslani HTTP dotazu typu GET response = requests.get(URL) assert response is not None > assert response.ok E assert False E + where False = <Response [404]>.ok 02_check_status_test.py:27: AssertionError =========================== short test summary info ============================ FAILED 02_check_status_test.py::test_get_method_for_missing_endpoint - assert... ========================= 1 failed, 1 passed in 0.88s ==========================
4. Příprava funkcí pro knihovnu určenou pro testování REST API
V předchozích testech už bylo patrné, že se některé operace – zejména podmínky – opakují. Z tohoto důvodu nemusí být špatným nápadem provést malý refaktoring a připravit si tak malou knihovnu určenou pro zjednodušení psaní REST API testů. Inspirací přitom je knihovna Frisby určená pro programovací jazyk Go, s níž jsme se seznámili v článku Testování aplikací psaných v Go s využitím knihoven Goblin a Frisby. Tato knihovna používá zřetězení funkcí, ovšem prozatím si vystačíme pouze s pomocnými funkcemi; například s funkcí, která pouze otestuje, zda server vrátil HTTP kód 200 OK či nikoli:
def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200
Zařazení takové funkce do testů je snadné, což je ostatně ukázáno i na dnešním třetím demonstračním příkladu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) def test_get_method_for_missing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/neexistuje" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response)
Nyní by měl nástroj Pytest vypsat informaci o tom, že první test byl dokončen úspěšně, ale druhý skončil s chybou:
$ pytest -v 03_refactor_check_status_test.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 2 items 03_refactor_check_status_test.py::test_get_method_for_existing_endpoint PASSED [ 50%] 03_refactor_check_status_test.py::test_get_method_for_missing_endpoint FAILED [100%] =================================== FAILURES =================================== _____________________ test_get_method_for_missing_endpoint _____________________ def test_get_method_for_missing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/neexistuje" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi > expect_ok_response(response) 03_refactor_check_status_test.py:32: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ response = <Response [404]> def expect_ok_response(response): assert response is not None > assert response.ok E assert False E + where False = <Response [404]>.ok 03_refactor_check_status_test.py:9: AssertionError =========================== short test summary info ============================ FAILED 03_refactor_check_status_test.py::test_get_method_for_missing_endpoint ========================= 1 failed, 1 passed in 0.88s ==========================
5. Kontrola hlavičky nebo hlaviček vrácených serverem v odpovědi
Samotné odpovědi serveru neobsahují pouze požadovaná data (payload), ale i metainformace o těchto datech. Tyto metainformace se ukládají do hlaviček odpovědi, ke kterým samozřejmě máme při použití knihovny Requests přístup a můžeme je tedy zkontrolovat i v testech REST API, například následujícím způsobem:
# test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == "application/json"
V dnešním čtvrtém demonstračním příkladu je výše uvedený úryvek programového kódu použit pro ověření, že server vrací odpověď se správným typem, tedy konkrétně data odpovídající MIME typu „application/json“:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) # precteni hlavicek headers = response.headers assert response is not None assert response.ok assert response.status_code == 200 # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == "application/json"
Výsledek získaný po spuštění testů:
$ pytest -v 04_check_content_type.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 04_check_content_type.py::test_get_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.53s ===============================
Samozřejmě nám nic nebrání si vyzkoušet chování v případě, že se vrací neočekávaný typ dat:
$ pytest -v 04_check_content_type_B.py ================================================= test session starts ================================================= platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collected 1 item 04_check_content_type_B.py::test_get_method_for_existing_endpoint FAILED [100%] ====================================================== FAILURES ======================================================= ________________________________________ test_get_method_for_existing_endpoint ________________________________________ def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) # precteni hlavicek headers = response.headers assert response is not None assert response.ok assert response.status_code == 200 # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky > assert headers["content-type"] == "text/plain" E AssertionError: assert 'application/json' == 'text/plain' E - text/plain E + application/json 04_check_content_type_B.py:25: AssertionError ========================================================= short test summary info ================================================== FAILED 04_check_content_type_B.py::test_get_method_for_existing_endpoint - AssertionError: assert 'application/json' == 'text/plain' ============================================================ 1 failed in 0.66s =====================================================
6. Samostatná funkce pro otestování typu odpovědi
Kontrola typu odpovědi (resp. přesněji MIME typu) je v praxi tak častá, že si pro ni můžeme vytvořit novou pomocnou funkci volanou z jednotlivých testů:
def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type
Upravený kód tohoto demonstračního příkladu s dvojicí testů vypadá následovně:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json")
Chování takto upraveného příkladu po spuštění:
$ pytest -v 05_refactor_check_content_type.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 05_refactor_check_content_type.py::test_get_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.47s ===============================
Opět je samozřejmě možné otestovat i negativní příklad (s chybami):
$ pytest -v 05_refactor_check_content_type_B.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 05_refactor_check_content_type_B.py::test_get_method_for_existing_endpoint FAILED [100%] =================================== FAILURES =================================== ____________________ test_get_method_for_existing_endpoint _____________________ def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) > expect_content_type(response, "text/plain") 05_refactor_check_content_type_B.py:33: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ response = <Response [200]>, content_type = 'text/plain' def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky > assert headers["content-type"] == content_type E AssertionError: assert 'application/json' == 'text/plain' E - text/plain E + application/json 05_refactor_check_content_type_B.py:21: AssertionError =========================== short test summary info ============================ FAILED 05_refactor_check_content_type_B.py::test_get_method_for_existing_endpoint ============================== 1 failed in 0.57s ===============================
7. Test, zda server poslal rastrové obrázky či podobná data
Mnoho serverů nevrací požadované informace pouze ve formátu JSON. Příkladem mohou být mapové servery vracející buď vektorové kresby ve formátu SVG nebo rastrové obrázky ve formátu PNG, GIF, popř. JPEG. I tyto typy informací mají přiřazen příslušný MIME typ, takže si náš test můžeme rozšířit o další typy dat, zde konkrétně o otestování, zda server vrátil rastrové obrázky ve formátech PNG a JPEG (pochopitelně při přístupu na příslušné koncové body):
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") def test_get_method_for_png_image(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/image/png" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "image/png") def test_get_method_for_jpeg_image(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/image/jpeg" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "image/jpeg")
Spuštění testů vůči existujícímu REST API serveru:
$ pytest -v 06_more_content_types.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 3 items 06_more_content_types.py::test_get_method_for_existing_endpoint PASSED [ 33%] 06_more_content_types.py::test_get_method_for_png_image PASSED [ 66%] 06_more_content_types.py::test_get_method_for_jpeg_image PASSED [100%] ============================== 3 passed in 1.49s ===============================
8. Otestování těla odpovědi
Testy REST API pochopitelně musí zjišťovat i to, jestli server skutečně vrátil požadovanou informaci či informace. Musíme tedy nejenom testovat obsah HTTP hlaviček odpovědi (což jsou metainformace), ale i obsah těla odpovědi. Pokud je odpověď čistě textová (nebo čistě binární), tj. bez složitější struktury, můžeme s tělem odpovědi pracovat přímo, a to přes tento atribut:
response.text
Nejjednodušší test může zjišťovat, jestli se v těle odpovědi nachází očekávaný řetězec. Samozřejmě můžeme použít regulární výrazy atd., ovšem test na přítomnost očekávaného (pod)řetězce lze v Pythonu napsat ještě stručněji:
def expect_text_in_payload(response, expected): assert expected in response.text
V dalším demonstračním příkladu se zjišťuje, jestli se v těle odpovědi nachází text „42“:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def expect_text_in_payload(response, expected): assert expected in response.text def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get?x=6&y=7&answer=42" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") expect_text_in_payload(response, "42")
Spuštění testů:
$ pytest -v 07_check_payload.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 07_check_payload.py::test_get_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.47s ===============================
Předchozí test skutečně prošel, a to z toho důvodu, že odpověď serveru obsahuje následující data:
{ "args": { "answer": "42", "x": "6", "y": "7" }, "headers": { "Host": "a0207c42-pmhttpbin-pmhttpb-c018-592832243.us-east-1.elb.amazonaws.com", "X-Amzn-Trace-Id": "Root=1-5f022b53-28ef633486278da51c263ebf" }, "origin": "37.48.51.80", "url": "http://a0207c42-pmhttpbin-pmhttpb-c018-592832243.us-east-1.elb.amazonaws.com/get?x=6&y=7&answer=42" }
9. Zpracování a otestování odpovědi serveru ve formátu JSON
Velmi často se setkáme s tím, že je nutné zpracovat nebo alespoň zkontrolovat odpověď serveru ve formátu JSON. Příkladem může být odpověď ve formátu, který byl ukázán na konci předchozí kapitoly. Jak tedy například zjistíme, že se správně vrátila hodnota atributu answer (tedy řetězec „42“), který je uložen v atributu args:
{ "args": { "answer": "42", ... ... ...
Jedno z možných řešení spočívá v otrockém přístupu k hodnotě atributu:
# pruchod datovou strukturou odeslanou serverem encoded = response.json() assert "args" in encoded args = encoded["args"] assert "answer" in args answer = args["answer"] assert answer == "42"
Popř. použít jednořádkovou variantu:
# pruchod datovou strukturou odeslanou serverem encoded = response.json() assert encoded["args"]["answer"] == "42"
Toto jednoduché otestování jednoho atributu odpovědi je ukázáno v další verzi demonstračního příkladu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get?x=6&y=7&answer=42" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # pruchod datovou strukturou odeslanou serverem encoded = response.json() assert "args" in encoded args = encoded["args"] assert "answer" in args answer = args["answer"] assert answer == "42"
S výsledky:
$ pytest -v 08_check_json_content.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 08_check_json_content.py::test_get_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.46s ===============================
10. Výběr atributů s využitím DSL
Při práci s formátem JSON můžeme narazit na úkol získat pouze jeden konkrétní element (nebo jeho hodnotu), a to i v případě, že je tento element zanořený ve složitější datové struktuře. Pro zjednodušení tohoto úkolu jsem pro jeden starší projekt vytvořil pomocnou funkci, která umožňuje zápis cesty k elementu, popř. v určení indexu (pokud se v JSONu používá pole), což je přístup, jenž je (pochopitelně v mnohem sofistikovanější podobě) použit i v rodině jazyků XML v nástrojích podporujících xpath. Podívejme se nyní na použití této funkce v pořadí již devátém demonstračním příkladu:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def get_value_using_path(obj, path): """Get the attribute value using the XMLpath-like path specification. Return any attribute stored in the nested object and list hierarchy using the 'path' where path consists of: keys (selectors) indexes (in case of arrays) separated by slash, ie. "key1/0/key_x". Usage: get_value_using_path({"x" : {"y" : "z"}}, "x")) -> {"y" : "z"} get_value_using_path({"x" : {"y" : "z"}}, "x/y")) -> "z" get_value_using_path(["x", "y", "z"], "0")) -> "x" get_value_using_path(["x", "y", "z"], "1")) -> "y" get_value_using_path({"key1" : ["x", "y", "z"], "key2" : ["a", "b", "c", "d"]}, "key1/1")) -> "y" get_value_using_path({"key1" : ["x", "y", "z"], "key2" : ["a", "b", "c", "d"]}, "key2/1")) -> "b" """ keys = path.split("/") for key in keys: if key.isdigit(): obj = obj[int(key)] else: obj = obj[key] return obj def test_get_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/get?x=6&y=7&answer=42" # poslani HTTP dotazu typu GET response = requests.get(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # ziskani konkretniho prvku z vracene datove struktury encoded = response.json() answer = get_value_using_path(encoded, "args/answer") assert answer == "42"
Chování si můžeme snadno ověřit spuštěním příslušného příkladu jako testu:
$ pytest -v 09_xpath_like_query.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 09_xpath_like_query.py::test_get_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.44s ===============================
11. Otestování REST API endpointu dostupného přes HTTP metodu POST
Vzhledem k tomu, že knihovna Requests podporuje všechny HTTP metody:
Metoda | Příklad použití |
---|---|
GET | Základní metoda sloužící k získání dat ze serveru. Může se například jednat o HTML stránku, statický obrázek, ale i výsledek volání REST API služby. |
POST | Metoda používaná pro odesílání dat na server. Teoreticky je sice možné použít i metodu GET, ovšem sémanticky je vhodnější použít tuto metodu (a REST API služby sémantiku operace většinou dodržují). |
PUT | Tato metoda slouží k nahrání dat na server. S touto metodou se setkáme u některých REST API služeb. |
DELETE | Slouží ke smazání dat ze serveru. Opět platí – s touto metodou se setkáme méně často u některých REST API služeb. |
HEAD | Tato metoda se částečně podobá metodě GET, ovšem server nevrátí požadovaná data, ale pouze metadata (čas změny, velikost dat, typ/formát dat apod.). Obecně je možné říci, že se tento dotaz zpracuje rychleji než GET. |
CONNECT | Používá se při použití TCP/IP tunelování. |
OPTIONS | Poslání dotazu na server, které metody podporuje. Využití najde například při použití CORS apod. |
TRACE | Server by měl klientovi odeslat požadavek zpět, čehož se používá pro zjištění, které údaje se mění na přenosové cestě. |
PATCH | Umožňuje změnu dat na serveru, má tady jinou sémantiku než DELETE+PUT. |
…můžeme si zkusit napsat velmi jednoduchý test založený na druhé nejpoužívanější metodě POST (zcela nejpoužívanější je díky existenci WWW metoda GET). Další demonstrační příklad vznikl jen nepatrnou úpravou příkladů předchozích:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def test_post_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/post" # poslani HTTP dotazu typu POST response = requests.post(URL) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json")
Opět si nezapomeneme celý demonstrační příklad spustit ve formě testu:
$ pytest -v 10_post_method_test.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 10_post_method_test.py::test_post_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.44s ===============================
12. Zaslání dat na server s využitím metody POST
První metoda poslání parametrů od klienta na server používá takzvané „formulářové položky“. Tento poněkud nepřesný název je odvozen od toho, že se podobným způsobem posílají data z HTML formuláře (bez použití JavaScriptu, pouze čistě HTML prostředky). Pokud budeme chtít simulovat posílání dat tímto způsobem, můžeme použít nepovinný parametr nazvaný data předaný funkci requests.post():
payload = { "klic": "hodnota", "answer": 42, "question": None, "correct": True} # poslani HTTP dotazu typu POST se specifikaci hodnot formulare response = requests.post(URL, data=payload)
Implementace tohoto postupu do demonstračního příkladu je dosti přímočará:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def test_post_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/post" # data posilana serveru payload = { "klic": "hodnota", "answer": 42, "question": None, "correct": True} # poslani HTTP dotazu typu POST s telem response = requests.post(URL, data=payload) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json")
S výsledky:
$ pytest -v 11_post_method_with_payload_test.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 11_post_method_with_payload_test.py::test_post_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.55s ===============================
13. Předání dat serveru v těle HTTP požadavku typu POST
Pokud budeme chtít serveru předat větší množství strukturovaných dat, a to potenciálně včetně speciálních hodnot, je lepší takové údaje předat přímo v těle požadavku. Pro tento účel se ve funkci requests.post() použije nepovinný parametr nazvaný json a nikoli parametr pojmenovaný data (jako tomu bylo v příkladu předchozím). Tento postup jsme si již ukázali v úvodním článku, takže jeho použití v testech bude snadné a přímočaré:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def test_post_method_for_existing_endpoint(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/post" # data posilana serveru payload = { "klic": "hodnota", "answer": 42, "question": None, "correct": True} # poslani HTTP dotazu typu POST s telem response = requests.post(URL, json=payload) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json")
S výsledky (očekávanými):
$ pytest -v 12_post_method_with_payload_test.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 12_post_method_with_payload_test.py::test_post_method_for_existing_endpoint PASSED [100%] ============================== 1 passed in 0.46s ===============================
14. Předání cookies v požadavku a test dat vrácených v odpovědi serveru
Funkce requests.get() umožňuje kromě typu požadavku (HTTP metoda), dat i metadat (typ, například „application/json“) specifikovat i cookies, což je téma, kterému jsme se podrobněji věnovali minule. Parametr, v němž se cookies předávají do požadavku posílaného na server, se jmenuje přímo cookies a předává se v něm buď slovník obsahující dvojice jméno_cookie+hodnota_cookie nebo objekt typu CookieJar. V dalším demonstračním příkladu je ukázáno, jak se reprezentují cookies formou slovníku, což je samozřejmě (alespoň v Pythonu) nejjednodušší řešení. Příklad na server pošle požadavek s cookies a server tyto cookies vrátí v těle odpovědi:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests import json def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def get_value_using_path(obj, path): """Get the attribute value using the XMLpath-like path specification. Return any attribute stored in the nested object and list hierarchy using the 'path' where path consists of: keys (selectors) indexes (in case of arrays) separated by slash, ie. "key1/0/key_x". Usage: get_value_using_path({"x" : {"y" : "z"}}, "x")) -> {"y" : "z"} get_value_using_path({"x" : {"y" : "z"}}, "x/y")) -> "z" get_value_using_path(["x", "y", "z"], "0")) -> "x" get_value_using_path(["x", "y", "z"], "1")) -> "y" get_value_using_path({"key1" : ["x", "y", "z"], "key2" : ["a", "b", "c", "d"]}, "key1/1")) -> "y" get_value_using_path({"key1" : ["x", "y", "z"], "key2" : ["a", "b", "c", "d"]}, "key2/1")) -> "b" """ keys = path.split("/") for key in keys: if key.isdigit(): obj = obj[int(key)] else: obj = obj[key] return obj def test_cookies(): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/cookies" # hlavicka posilana v dotazu headers = {'accept': 'application/json'} # priprava cookies cookies = {'key1': 'value1', 'key2': 'value2', 'key3': 'value3'} # poslani HTTP dotazu typu GET response = requests.get(URL, headers=headers, cookies=cookies) # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # precteni hlavicek headers = response.headers # pruchod datovou strukturou odeslanou serverem encoded = response.json() cookies = get_value_using_path(encoded, "cookies") # test existence klicu assert "key1" in cookies assert "key2" in cookies assert "key3" in cookies # test hodnot, ktere server detekoval v poslanych cookies assert get_value_using_path(encoded, "cookies/key1") == "value1" assert get_value_using_path(encoded, "cookies/key2") == "value2" assert get_value_using_path(encoded, "cookies/key3") == "value3" # nyni ziskame cookies z odpovedi a zkontrolujeme jejich obsah cookies = response.cookies.get_dict() # melo by se jednat o prazdny slovnik assert not cookies
Tento příklad si samozřejmě opět můžeme spustit nástrojem Pytest:
$ pytest -v 13_cookies_tests.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 1 item 13_cookies_tests.py::test_cookies PASSED [100%] ============================== 1 passed in 0.49s ===============================
15. Správa sezení s využitím cookies
Cookies se používají kromě dalších věcí i ve chvíli, kdy je zapotřebí si nějakým způsobem zapamatovat stav nějaké sekvence operací, resp. stavu (opět viz předchozí článek). Typickým příkladem je webový obchod, u něhož si samozřejmě musíme pamatovat přihlášeného uživatele, obsah jeho košíku, zda již bylo za zboží zaplaceno atd. (ostatně podobnou technologii používá i Root). Ve chvíli, kdy je klientská část naprogramována s využitím knihovny Requests, je možné celý stav (možná lépe řečeno „sezení“) reprezentovat objektem typu Session. Požadavky na server se pak posílají odlišně – provádějí se nikoli zavoláním request._http_metoda_, ale session._http_metoda_. Jak ovšem takové sezení vytvořit v testech? V tomto případě se nabízí použití speciální funkce setup_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. První varianta příkladu je založena na použití globální proměnné session, což je sice korektní, ovšem nepříliš idiomatické:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import requests import json session = None def expect_ok_response(response): assert response is not None assert response.ok assert response.status_code == 200 def expect_content_type(response, content_type): # precteni hlavicek headers = response.headers # test existence hlavicky assert "content-type" in headers # kontrola obsahu hlavicky assert headers["content-type"] == content_type def expect_cookie(response, name, value): cookies = session.cookies assert name in cookies assert cookies[name] == value def expect_cookies(response, how_many): cookies = session.cookies assert len(cookies) == how_many def get_value_using_path(obj, path): """Get the attribute value using the XMLpath-like path specification. Return any attribute stored in the nested object and list hierarchy using the 'path' where path consists of: keys (selectors) indexes (in case of arrays) separated by slash, ie. "key1/0/key_x". Usage: get_value_using_path({"x" : {"y" : "z"}}, "x")) -> {"y" : "z"} get_value_using_path({"x" : {"y" : "z"}}, "x/y")) -> "z" get_value_using_path(["x", "y", "z"], "0")) -> "x" get_value_using_path(["x", "y", "z"], "1")) -> "y" get_value_using_path({"key1" : ["x", "y", "z"], "key2" : ["a", "b", "c", "d"]}, "key1/1")) -> "y" get_value_using_path({"key1" : ["x", "y", "z"], "key2" : ["a", "b", "c", "d"]}, "key2/1")) -> "b" """ keys = path.split("/") for key in keys: if key.isdigit(): obj = obj[int(key)] else: obj = obj[key] return obj def set_cookie(session, name, value): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/cookies/set/{name}/{value}".format(name=name, value=value) # hlavicka posilana v dotazu headers = {'accept': 'application/json'} # poslani HTTP dotazu typu GET return session.get(URL, headers=headers) def delete_cookie(session, name): # adresa s testovaci REST API sluzbou URL = "http://httpbin.org/cookies/delete?{name}=".format(name=name) # hlavicka posilana v dotazu headers = {'accept': 'application/json'} # poslani HTTP dotazu typu GET return session.get(URL, headers=headers) def setup_module(module): global session session = requests.Session() def test_set_cookie_1(): response = set_cookie(session, "foo", "6") # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # test cookies expect_cookies(response, 1) expect_cookie(response, "foo", "6") def test_set_cookie_2(): response = set_cookie(session, "bar", "7") # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # test cookies expect_cookies(response, 2) expect_cookie(response, "foo", "6") expect_cookie(response, "bar", "7") def test_set_cookie_3(): response = set_cookie(session, "foo", "42") # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # test cookies expect_cookies(response, 2) expect_cookie(response, "foo", "42") expect_cookie(response, "bar", "7") def test_delete_cookie_1(): response = delete_cookie(session, "foo") # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # test cookies expect_cookies(response, 1) expect_cookie(response, "bar", "7") def test_delete_cookie_2(): response = delete_cookie(session, "baz") # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # test cookies expect_cookies(response, 1) expect_cookie(response, "bar", "7") def test_delete_cookie_3(): response = delete_cookie(session, "bar") # zakladni test odpovedi expect_ok_response(response) expect_content_type(response, "application/json") # test cookies expect_cookies(response, 0)
Výsledky:
$ pytest -v 14_session_cookies_tests.py ============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 6 items 14_session_cookies_tests.py::test_set_cookie_1 PASSED [ 16%] 14_session_cookies_tests.py::test_set_cookie_2 PASSED [ 33%] 14_session_cookies_tests.py::test_set_cookie_3 PASSED [ 50%] 14_session_cookies_tests.py::test_delete_cookie_1 PASSED [ 66%] 14_session_cookies_tests.py::test_delete_cookie_2 PASSED [ 83%] 14_session_cookies_tests.py::test_delete_cookie_3 PASSED [100%] ============================== 6 passed in 3.03s ===============================
def setup_module(module): module.session = requests.Session()
16. Kontrola složitějších datových struktur vracených serverem
Mnoho testů REST API navíc vyžaduje provedení minimálně tří, popř. čtyř typů operací:
- Kontrola, popř. použití JWT tokenů či podobné technologie. Tímto důležitým tématem se budeme zabývat později.
- Validace prakticky libovolně komplikovaných datových struktur vrácených serverem, a to na základě programátorem definovaného schématu.
- Načítání (parsování) obsahu reprezentovaného pomocí XML, přístup k jednotlivým prvkům výsledného stromu.
- Dtto, ovšem pro obsah (typicky dokument) reprezentovaný v HTML.
Nejdříve se alespoň ve stručnosti zmiňme o validaci datových struktur, tj. většinou struktur reprezentovaných v JSONu, popř. (méně často) v jiném serializačním formátu. Pro programovací jazyk Python existuje několik knihoven, které většinou zajišťují dvě úlohy:
- provedení samotné validace (musíme vědět, jak mají data vypadat)
- současně strukturu dat dokumentuje, a to rigidním způsobem
Validaci dat je možné využít v mnoha oblastech. Představme si například dokumentovou databázi, složitý konfigurační soubor nebo asi nejlépe klasickou webovou službu, která přijme data ve formátu JSON, převede je knihovní funkcí do nativní datové struktury (typicky do slovníku seznamů či hierarchicky uspořádaných slovníků) a následně provede validaci této struktury, ovšem nikoli programově (testováním jednotlivých atributů), ale na základě deklarativního popisu této struktury. Například můžeme specifikovat, že v atributu nazvaném „price“ by mělo být uloženo nezáporné číslo menší než 100000, v atributu pojmenovaném „valid_from“ musí být uložen řetězec odpovídající skutečnému datu (to už nelze otestovat primitivním regulárním výrazem, ale složitějším predikátem) a v atributu „login“ bude buď nick uživatele nebo bude tento atribut obsahovat null/None (popř. alternativně nebude existovat vůbec).
Pro Python jsou poskytovány mj. i tyto knihovny:
- Schemagic
- Schema
- Voluptuous
Některé příklady použití těchto knihoven si ukážeme příště společně s jednoduchým REST API serverem, který bude poskytovat data v příslušném formátu.
============================= test session starts ============================== platform linux -- Python 3.6.6, pytest-5.4.2, py-1.5.2, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/ptisnovs/src/python/testing-in-python/requests/tests plugins: print-0.1.3, voluptuous-1.0.2 collecting ... collected 2 items 01_basic_usage_test.py::test_get_method_for_existing_endpoint PASSED [ 50%] 01_basic_usage_test.py::test_get_method_for_missing_endpoint PASSED [100%] ============================== 2 passed in 0.71s ===============================
17. Zpracování XML a HTML v Pythonu s využitím knihovny lxml
Další často prováděnou operací při testování REST API je zpracování (či možná lépe řečeno kontrola) dat reprezentovaných v jazyku XML či HTML. K tomuto účelu může velmi dobře posloužit knihovna lxml, o níž se dnes pro úplnost jen krátce zmíníme. Tato knihovna slouží k načítání (parsování) XML souborů, přístupu k jednotlivým prvkům výsledného stromu, tvorbě a zapisování nových XML a v případě potřeby lze tuto knihovnu použít i pro zpracování HTML stránek. Zajímavé je, že se tato knihovna poměrně dobře hodí i pro práci s nevalidními XML, XML bez schématu atd. – tj. se soubory, které může být obtížné zpracovat v jiných nástrojích – a právě proto se velmi dobře hodí do testů. Vývojářům jsou v případě potřeby k dispozici i další zajímavé technologie, zejména XPath (zjednodušeně: přístup k elementům a jejich atributům přes doménově specifický jazyk) a SAX, tj. možnost zpracovávat XML jako sekvenci elementů, což je přístup mnohem méně náročný na paměť. Navíc se většinou jedná o rychlejší způsob práce s XML.
Knihovnu lxml je možné v případě potřeby použít i pro zpracování HTML stránek. Při zpracovávání HTML stránek se nevyžaduje (a popravdě řečeno ani neočekává) validita stránky, takže se specializovaný parser nazvaný HTMLParser snaží z dodaného zdrojového kódu stránky získat korektní stromovou strukturu, a to i v případě, že autor například neuzavírá značky, nevkládá uzavírací značky ve správném pořadí atd. Podívejme se nyní na jednoduchý příklad zpracovatelné HTML stránky, konkrétně na stránku dostupnou na adrese http://www.zyvra.org/html/simple.htm. Zdrojový kód této stránky vypadá následovně (stylem zápisu trošku připomíná minulé tisíciletí :-):
<html><head><title> Very simple HTML page. </title></head> <body> <p>You can look at the source of this page by: Right clicking anywhere out in space on this page then selecting "View" in the menu.</p> <p>This works on any page, but sometimes what you see may be very complex and seem confusing.</p> <p> <b>Please,</b> look at the source and what you see with the browser. You should understand and see the effect of every tag. Use the little Icons up in the right of your browser screen to change the size of the window and see the effect, and how the browser displays this page.</p> <p align="right"> Yes, this is a <b>Very Plain</b> page. <i>But it works!</i></p> <p><i><b>Remember. We are just getting started,</b> and I haven't used anything more than I have talked about in a couple pages!</i> Yes. You will want to be more fancy. Just be patient, we'll get there. <p align="center">Now create a page like this of your own. <b>Have fun!</b></p </body></html>
Povšimněte si například toho, že se ve stránce objevují neuzavřené značky <p>, chybí informace o verzi HTML, o kódování atd. I přesto je možné takovou stránku zpracovat.
Realizace příkladu pro zpracování HTML stránky je poměrně přímočará. Nejprve (přesněji řečeno po importu lxml.etree) vytvoříme instanci parseru HTML stránek:
import lxml.etree as ET parser = ET.HTMLParser()
Dále se pokusíme stránku načíst a ihned poté zparsovat. Povšimněte si, že funkce lxml.etree.parse rozpozná URL a stránku v případě potřeby stáhne (pokud ovšem není dostupná na serveru s HTTPS!):
url = "http://www.zyvra.org/html/simple.htm" tree = ET.parse(url, parser) root = tree.getroot()
Nyní, když máme k dispozici celý strom i kořenový prvek, se můžeme pokusit zpětně zrekonstruovat řetězec se zdrojovým kódem stránky:
print("\n\n\nContent:") result = ET.tostring(tree.getroot(), pretty_print=True, method="html") print(result)
Podrobnosti, včetně testů určených pro reálný server, si ukážeme příště.
18. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/testing-in-python. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady a jejich části, které naleznete v následující tabulce:
19. Předchozí články s tématem testování (nejenom) v Pythonu
Tématem testování jsme se již na stránkách Rootu několikrát zabývali. Jedná se mj. o následující články:
- Použití Pythonu pro tvorbu testů: od jednotkových testů až po testy UI
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-od-jednotkovych-testu-az-po-testy-ui/ - Použití Pythonu pro tvorbu testů: použití třídy Mock z knihovny unittest.mock
https://www.root.cz/clanky/pouziti-pythonu-pro-tvorbu-testu-pouziti-tridy-mock-z-knihovny-unittest-mock/ - Použití nástroje pytest pro tvorbu jednotkových testů a benchmarků
https://www.root.cz/clanky/pouziti-nastroje-pytest-pro-tvorbu-jednotkovych-testu-a-benchmarku/ - Nástroj pytest a jednotkové testy: fixtures, výjimky, parametrizace testů
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-fixtures-vyjimky-parametrizace-testu/ - Nástroj pytest a jednotkové testy: životní cyklus testů, užitečné tipy a triky
https://www.root.cz/clanky/nastroj-pytest-a-jednotkove-testy-zivotni-cyklus-testu-uzitecne-tipy-a-triky/ - Struktura projektů s jednotkovými testy, využití Travis CI
https://www.root.cz/clanky/struktura-projektu-s-jednotkovymi-testy-vyuziti-travis-ci/ - Omezení stavového prostoru testovaných funkcí a metod
https://www.root.cz/clanky/omezeni-stavoveho-prostoru-testovanych-funkci-a-metod/ - Testování aplikací s využitím nástroje Hypothesis
https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis/ - Testování aplikací s využitím nástroje Hypothesis (dokončení)
https://www.root.cz/clanky/testovani-aplikaci-s-vyuzitim-nastroje-hypothesis-dokonceni/ - Testování webových aplikací s REST API z Pythonu
https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu/ - Testování webových aplikací s REST API z Pythonu (2)
https://www.root.cz/clanky/testovani-webovych-aplikaci-s-rest-api-z-pythonu-2/ - 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
- Requests: HTTP for Humans (dokumentace)
http://docs.python-requests.org/en/master/ - Requests: Introduction
http://docs.python-requests.org/en/latest/user/intro/ - Requests na GitHubu
https://github.com/requests/requests - Requests (software) na Wikipedii
https://en.wikipedia.org/wiki/Requests_%28software%29 - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - 20 Python libraries you can’t live without
https://pythontips.com/2013/07/30/20-python-libraries-you-cant-live-without/ - What are the top 10 most useful and influential Python libraries and frameworks?
https://www.quora.com/What-are-the-top-10-most-useful-and-influential-Python-libraries-and-frameworks - Python: useful modules
https://wiki.python.org/moin/UsefulModules - Top 15 most popular Python libraries
https://keyua.org/blog/most-popular-python-libraries/ - Hypertext Transfer Protocol
https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol - List of HTTP header fields
https://en.wikipedia.org/wiki/List_of_HTTP_header_fields - List of HTTP status codes
https://en.wikipedia.org/wiki/List_of_HTTP_status_codes - Python requests deep dive
https://medium.com/@anthonypjshaw/python-requests-deep-dive-a0a5c5c1e093 - The awesome requests module
https://www.pythonforbeginners.com/requests/the-awesome-requests-module - Send HTTP Requests in Python
https://code-maven.com/http-requests-in-python - Introducing JSON
http://json.org/ - Writing tests for RESTful APIs in Python using requests – part 1: basic tests
https://www.ontestautomation.com/writing-tests-for-restful-apis-in-python-using-requests-part-1-basic-tests/ - Step by Step Rest API Testing using Python + Pytest + Allure
https://www.udemy.com/course/api-testing-python/ - Prime formulas and polynomial functions
https://en.wikipedia.org/wiki/Formula_for_primes#Prime_formulas_and_polynomial_functions - Prime-Generating Polynomial
https://mathworld.wolfram.com/Prime-GeneratingPolynomial.html - Hoare logic
https://en.wikipedia.org/wiki/Hoare_logic - Goto Fail, Heartbleed, and Unit Testing Culture
https://martinfowler.com/articles/testing-culture.html - PEP-484
https://www.python.org/dev/peps/pep-0484/ - In-depth: Functional programming in C++
https://www.gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php - mypy
http://www.mypy-lang.org/ - Welcome to Mypy documentation!
https://mypy.readthedocs.io/en/latest/index.html - mypy na GitHubu
https://github.com/python/mypy - mypy 0.770 na PyPi
https://pypi.org/project/mypy/ - Extensions for mypy (separated out from mypy/extensions)
https://github.com/python/mypy_extensions - The Mypy Blog
https://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html - Our journey to type checking 4 million lines of Python
https://dropbox.tech/application/our-journey-to-type-checking-4-million-lines-of-python - Type-Checking Python Programs With Type Hints and mypy
https://www.youtube.com/watch?v=2×WhaALHTvU - Refactoring to Immutability – Kevlin Henney
https://www.youtube.com/watch?v=APUCMSPiNh4 - Bernat Gabor – Type hinting (and mypy) – PyCon 2019
https://www.youtube.com/watch?v=hTrjTAPnA_k - Stanford Seminar – Optional Static Typing for Python
https://www.youtube.com/watch?v=GiZKuyLKvAA - mypy Getting to Four Million Lines of Typed Python – Michael Sullivan
https://www.youtube.com/watch?v=FT_WHV4-QcU - Shebang
https://en.wikipedia.org/wiki/Shebang_(Unix) - pytest 5.4.2 na PyPi
https://pypi.org/project/pytest/ - Hillel Wayne – Beyond Unit Tests: Taking Your Testing to the Next Level – PyCon 2018
https://www.youtube.com/watch?v=MYucYon2-lk - Awesome Python – testing
https://github.com/vinta/awesome-python#testing - pytest Plugins Compatibility
http://plugincompat.herokuapp.com/ - Selenium (pro Python)
https://pypi.org/project/selenium/ - Getting Started With Testing in Python
https://realpython.com/python-testing/ - unittest.mock — mock object library
https://docs.python.org/3.5/library/unittest.mock.html - mock 2.0.0
https://pypi.python.org/pypi/mock - An Introduction to Mocking in Python
https://www.toptal.com/python/an-introduction-to-mocking-in-python - Mock – Mocking and Testing Library
http://mock.readthedocs.io/en/stable/ - Python Mocking 101: Fake It Before You Make It
https://blog.fugue.co/2016–02–11-python-mocking-101.html - Nauč se Python! – Testování
http://naucse.python.cz/lessons/intro/testing/ - Flexmock (dokumentace)
https://flexmock.readthedocs.io/en/latest/ - Test Fixture (Wikipedia)
https://en.wikipedia.org/wiki/Test_fixture - Mock object (Wikipedia)
https://en.wikipedia.org/wiki/Mock_object - Extrémní programování
https://cs.wikipedia.org/wiki/Extr%C3%A9mn%C3%AD_programov%C3%A1n%C3%AD - Programování řízené testy
https://cs.wikipedia.org/wiki/Programov%C3%A1n%C3%AD_%C5%99%C3%ADzen%C3%A9_testy - Pip (dokumentace)
https://pip.pypa.io/en/stable/ - Tox
https://tox.readthedocs.io/en/latest/ - pytest: helps you write better programs
https://docs.pytest.org/en/latest/ - doctest — Test interactive Python examples
https://docs.python.org/dev/library/doctest.html#module-doctest - unittest — Unit testing framework
https://docs.python.org/dev/library/unittest.html - Python namespaces
https://bytebaker.com/2008/07/30/python-namespaces/ - Namespaces and Scopes
https://www.python-course.eu/namespaces.php - Stránka projektu Robot Framework
https://robotframework.org/ - GitHub repositář Robot Frameworku
https://github.com/robotframework/robotframework - Robot Framework (Wikipedia)
https://en.wikipedia.org/wiki/Robot_Framework - Tutoriál Robot Frameworku
http://www.robotframeworktutorial.com/ - Robot Framework Documentation
https://robotframework.org/robotframework/ - Robot Framework Introduction
https://blog.testproject.io/2016/11/22/robot-framework-introduction/ - robotframework 3.1.2 na PyPi
https://pypi.org/project/robotframework/ - Robot Framework demo (GitHub)
https://github.com/robotframework/RobotDemo - Robot Framework web testing demo using SeleniumLibrary
https://github.com/robotframework/WebDemo - Robot Framework for Mobile Test Automation Demo
https://www.youtube.com/watch?v=06LsU08slP8 - Gherkin
https://cucumber.io/docs/gherkin/ - Selenium
https://selenium.dev/ - SeleniumLibrary
https://robotframework.org/ - The Practical Test Pyramid
https://martinfowler.com/articles/practical-test-pyramid.html - Acceptance Tests and the Testing Pyramid
http://www.blog.acceptancetestdrivendevelopment.com/acceptance-tests-and-the-testing-pyramid/ - Tab-separated values
https://en.wikipedia.org/wiki/Tab-separated_values - A quick guide about Python implementations
https://blog.rmotr.com/a-quick-guide-about-python-implementations-aa224109f321 - radamsa
https://gitlab.com/akihe/radamsa - Fuzzing (Wikipedia)
https://en.wikipedia.org/wiki/Fuzzing - american fuzzy lop
http://lcamtuf.coredump.cx/afl/ - Fuzzing: the new unit testing
https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1 - Corpus for github.com/dvyukov/go-fuzz examples
https://github.com/dvyukov/go-fuzz-corpus - AFL – QuickStartGuide.txt
https://github.com/google/AFL/blob/master/docs/QuickStartGuide.txt - Introduction to Fuzzing in Python with AFL
https://alexgaynor.net/2015/apr/13/introduction-to-fuzzing-in-python-with-afl/ - Writing a Simple Fuzzer in Python
https://jmcph4.github.io/2018/01/19/writing-a-simple-fuzzer-in-python/ - How to Fuzz Go Code with go-fuzz (Continuously)
https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/ - Golang Fuzzing: A go-fuzz Tutorial and Example
http://networkbit.ch/golang-fuzzing/ - Fuzzing Python Modules
https://stackoverflow.com/questions/20749026/fuzzing-python-modules - 0×3 Python Tutorial: Fuzzer
http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/ - fuzzing na PyPi
https://pypi.org/project/fuzzing/ - Fuzzing 0.3.2 documentation
https://fuzzing.readthedocs.io/en/latest/ - Randomized testing for Go
https://github.com/dvyukov/go-fuzz - HTTP/2 fuzzer written in Golang
https://github.com/c0nrad/http2fuzz - Ffuf (Fuzz Faster U Fool) – An Open Source Fast Web Fuzzing Tool
https://hacknews.co/hacking-tools/20191208/ffuf-fuzz-faster-u-fool-an-open-source-fast-web-fuzzing-tool.html - Continuous Fuzzing Made Simple
https://fuzzit.dev/ - Halt and Catch Fire
https://en.wikipedia.org/wiki/Halt_and_Catch_Fire#Intel_x86 - Random testing
https://en.wikipedia.org/wiki/Random_testing - Monkey testing
https://en.wikipedia.org/wiki/Monkey_testing - Fuzzing for Software Security Testing and Quality Assurance, Second Edition
https://books.google.at/books?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%22I+settled+on+the+term+fuzz%22&redir_esc=y&hl=de#v=onepage&q=%22I%20settled%20on%20the%20term%20fuzz%22&f=false - libFuzzer – a library for coverage-guided fuzz testing
https://llvm.org/docs/LibFuzzer.html - fuzzy-swagger na PyPi
https://pypi.org/project/fuzzy-swagger/ - fuzzy-swagger na GitHubu
https://github.com/namuan/fuzzy-swagger - Fuzz testing tools for Python
https://wiki.python.org/moin/PythonTestingToolsTaxonomy#Fuzz_Testing_Tools - A curated list of awesome Go frameworks, libraries and software
https://github.com/avelino/awesome-go - gofuzz: a library for populating go objects with random values
https://github.com/google/gofuzz - tavor: A generic fuzzing and delta-debugging framework
https://github.com/zimmski/tavor - hypothesis na GitHubu
https://github.com/HypothesisWorks/hypothesis - Hypothesis: Test faster, fix more
https://hypothesis.works/ - Hypothesis
https://hypothesis.works/articles/intro/ - What is Hypothesis?
https://hypothesis.works/articles/what-is-hypothesis/ - What is Property Based Testing?
https://hypothesis.works/articles/what-is-property-based-testing/ - Databáze CVE
https://www.cvedetails.com/ - Fuzz test Python modules with libFuzzer
https://github.com/eerimoq/pyfuzzer - Taof – The art of fuzzing
https://sourceforge.net/projects/taof/ - JQF + Zest: Coverage-guided semantic fuzzing for Java
https://github.com/rohanpadhye/jqf - http2fuzz
https://github.com/c0nrad/http2fuzz - Demystifying hypothesis testing with simple Python examples
https://towardsdatascience.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294 - Testování
http://voho.eu/wiki/testovani/ - Unit testing (Wikipedia.en)
https://en.wikipedia.org/wiki/Unit_testing - Unit testing (Wikipedia.cz)
https://cs.wikipedia.org/wiki/Unit_testing - Unit Test vs Integration Test
https://www.youtube.com/watch?v=0GypdsJulKE - TestDouble
https://martinfowler.com/bliki/TestDouble.html - Test Double
http://xunitpatterns.com/Test%20Double.html - Test-driven development (Wikipedia)
https://en.wikipedia.org/wiki/Test-driven_development - Acceptance test–driven development
https://en.wikipedia.org/wiki/Acceptance_test%E2%80%93driven_development - Gauge
https://gauge.org/ - Gauge (software)
https://en.wikipedia.org/wiki/Gauge_(software) - PYPL PopularitY of Programming Language
https://pypl.github.io/PYPL.html - Testing is Good. Pyramids are Bad. Ice Cream Cones are the Worst
https://medium.com/@fistsOfReason/testing-is-good-pyramids-are-bad-ice-cream-cones-are-the-worst-ad94b9b2f05f - Články a zprávičky věnující se Pythonu
https://www.root.cz/n/python/ - PythonTestingToolsTaxonomy
https://wiki.python.org/moin/PythonTestingToolsTaxonomy - Top 6 BEST Python Testing Frameworks [Updated 2020 List]
https://www.softwaretestinghelp.com/python-testing-frameworks/ - pytest-print 0.1.3
https://pypi.org/project/pytest-print/ - pytest fixtures: explicit, modular, scalable
https://docs.pytest.org/en/latest/fixture.html - PyTest Tutorial: What is, Install, Fixture, Assertions
https://www.guru99.com/pytest-tutorial.html - Pytest – Fixtures
https://www.tutorialspoint.com/pytest/pytest_fixtures.htm - Marking test functions with attributes
https://docs.pytest.org/en/latest/mark.html - pytest-print
https://pytest-print.readthedocs.io/en/latest/ - Continuous integration
https://en.wikipedia.org/wiki/Continuous_integration - Travis CI
https://travis-ci.org/ - Mutation testing
https://en.wikipedia.org/wiki/Mutation_testing - Články o Hypothesis
https://news.ycombinator.com/from?site=hypothesis.works - Testovací případ
https://cs.wikipedia.org/wiki/Testovac%C3%AD_p%C5%99%C3%ADpad - Most testing is ineffective
https://hypothesis.works/