Obsah

Úvod do testovania kódu Pythonu pomocou PyTestu

PyTest je populárna testovacia knižnica pre Python. Je to jednoduchý, ľahko použiteľný testovací rámec, ktorý vám umožňuje písať a vykonávať testy pre váš kód v jazyku Python. Tu je stručný úvod do používania PyTestu.

Test je kód, ktorý vykonáva kód. Keď začnete vyvíjať novú funkciu pre svoj projekt v jazyku Python, môžete jej požiadavky formalizovať ako kód. Keď to urobíte, nielenže zdokumentujete spôsob, akým sa má kód vašej implementácie používať, ale môžete tiež automaticky spustiť všetky testy, aby ste sa vždy uistili, že váš kód zodpovedá vašim požiadavkám. Jedným z takýchto nástrojov, ktorý vám pri tom pomáha, je pytest a je to pravdepodobne najpopulárnejší testovací nástroj vo vesmíre Pythonu.

Je to všetko o assert

Predpokladajme, že ste napísali funkciu, ktorá overuje e-mailovú adresu. Všimnite si, že tu zostaneme jednoduchí a na overovanie e-mailových adries nepoužijeme regulárne výrazy ani testovanie DNS. Namiesto toho sa len uistíme, že sa v testovanom reťazci nachádza presne jeden znak @ a iba znaky latinky, čísla a znaky ., - a _.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import string

def is_valid_email_address(s):
    s = s.lower()
    parts = s.split('@')
    if len(parts) != 2:
      # Not exactly one at-sign
      return False
    allowed = set(string.ascii_lowercase + string.digits + '.-_')
    for part in parts:
        if not set(part) <= allowed:
          # Characters other than the allowed ones are found
          return False
    return True

Teraz máme v našom kóde niekoľko potvrdení. Napríklad tvrdíme, že tieto e-mailové adresy sú platné:

Na druhej strane by sme očakávali, že naša funkcia vráti False pre e-mailové adresy ako:

Môžeme skontrolovať, či sa naša funkcia skutočne správa tak, ako očakávame:

1
2
3
4
5
6
print(is_valid_email_address('[email protected]'))               # True
print(is_valid_email_address('[email protected]'))  # True
print(is_valid_email_address('[email protected]'))     # True
print(is_valid_email_address('not [email protected]'))          # False
print(is_valid_email_address('john.doe'))                       # False
print(is_valid_email_address('john,[email protected]'))           # False

Tieto príklady e-mailových adries, ktoré sme vymysleli, sa nazývajú testovacie prípady. Pre každý testovací prípad očakávame určitý výsledok. Nástroj ako pytest môže pomôcť automatizovať testovanie týchto tvrdení. Zapísanie týchto tvrdení vám môže pomôcť:

  • zdokumentovať, ako sa bude váš kód používať
  • uistiť sa, že budúce zmeny nerozbijú iné časti vášho softvéru
  • premýšľať o možných okrajových prípadoch vašich funkcií

Aby sme to dokázali, stačí vytvoriť nový súbor pre všetky naše testy a vložiť doň niekoľko funkcií.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def test_regular_email_validates():
    assert is_valid_email_address('[email protected]')
    assert is_valid_email_address('[email protected]')
    assert is_valid_email_address('[email protected]')

def test_valid_email_has_one_at_sign():
    assert not is_valid_email_address('john.doe')

def test_valid_email_has_only_allowed_chars():
    assert not is_valid_email_address('john,[email protected]')
    assert not is_valid_email_address('not [email protected]')

Spustenie testov

Jednoduchý príklad

V adresári projektu máme dva súbory: validator.py a test_validator.py.

Teraz môžeme jednoducho spustiť pytest z príkazového riadku. Jeho výstup by mal vyzerať približne takto:

1
2
3
4
5
6
7
8
======================= test session starts =========================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 3 items

test_validator.py ...                                          [100%]

======================== 3 passed in 0.01s ==========================

Tu nás pytest informuje, že našiel tri testovacie funkcie vo vnútri súboru test_validator.py a že všetky tieto funkcie boli odovzdané (ako je označené tromi bodkami ...).

Indikátor 100% nám dáva dobrý pocit, pretože sme si istí, že náš validátor funguje podľa očakávania. Ako sme však načrtli v úvode, funkcia validátora má ďaleko od dokonalosti. Rovnako ako naše testovacie prípady. Aj bez testovania DNS by sme e-mailovú adresu ako [email protected] označili za platnú, zatiaľ čo adresa ako [email protected] by bola označená za neplatnú.

Pridajme teraz tieto testovacie prípady do nášho súboru test_validator.py

1
2
3
4
5
6
...
def test_valid_email_can_have_plus_sign():
    assert is_valid_email_address('[email protected]')

def test_valid_email_must_have_a_tld():
    assert not is_valid_email_address('[email protected]')

Ak znovu spustíme pytest, uvidíme neúspešné testy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 5 items

test_validator.py ...FF                                                  [100%]

=================================== FAILURES ===================================
_____________________ test_valid_email_can_have_plus_sign ______________________

    def test_valid_email_can_have_plus_sign():
>       assert is_valid_email_address('[email protected]')
E       AssertionError: assert False
E        +  where False = is_valid_email_address('[email protected]')

test_validator.py:17: AssertionError
_______________________test_valid_email_must_have_a_tld_______________________

    def test_valid_email_must_have_a_tld():
>       assert not is_valid_email_address('[email protected]')
E       AssertionError: assert not True
E        +  where True = is_valid_email_address('[email protected]')

test_validator.py:20: AssertionError
=========================== short test summary info ============================
FAILED test_validator.py::test_valid_email_can_have_plus_sign - AssertionErro...
FAILED test_validator.py::test_valid_email_must_have_a_tld - AssertionError: ...
========================= 2 failed, 3 passed in 0.05s ==========================

Všimnite si, že okrem troch bodiek ... sme dostali aj dve FF, ktoré znamenajú, že dve testovacie funkcie zlyhali.

Okrem toho sme vo výstupe získali novú časť FAILURES, ktorá podrobne vysvetľuje, v ktorom bode náš test zlyhal. To je dosť užitočné pri ladení.

Náš malý príklad validátora je dôkazom dôležitosti navrhovania testov.

Najprv sme napísali našu funkciu validátora a potom sme pre ňu vymysleli niekoľko testovacích prípadov. Čoskoro sme si všimli, že tieto testovacie prípady nie sú v žiadnom prípade komplexné. Naopak, vynechali sme niektoré podstatné aspekty validácie e-mailovej adresy.

Možno ste už počuli o Test Driven Development (TDD), ktorý presadzuje presný opak: TDD je princíp, ktorý hovorí o správnom nastavení požiadaviek tým, že najprv napíšete testovacie prípady a nezačnete implementovať funkciu skôr, než budete mať pocit, že ste pokryli všetky testovacie prípady. Tento spôsob myslenia bol vždy dobrou myšlienkou, ale postupom času nadobudol ešte väčší význam, pretože softvérové projekty sa stali zložitejšími.

O TDD napíšem čoskoro ďalší blogový príspevok, v ktorom sa mu budem venovať podrobnejšie.

Konfigurácia

Nastavenie projektu je zvyčajne oveľa zložitejšie ako len jeden súbor s funkciou validátora.

Možno máte pre svoj projekt štruktúru balíkov Python alebo váš kód závisí od externých závislostí, napríklad od databázy.

Fixtures

Pojem fixture ste možno použili v rôznych kontextoch. Napríklad v prípade webového frameworku Django sa fixtures vzťahujú na kolekciu počiatočných údajov, ktoré sa majú načítať do databázy. V kontexte pytestu sa však fixtures vzťahujú len na funkcie spúšťané pomocou pytestu pred a/alebo po skutočných testovacích funkciách.

Nastavenie a zrušenie

Takéto funkcie môžeme vytvoriť pomocou dekorátora pytest.fixture(). Zatiaľ to robíme vo vnútri súboru test_validator.py.

1
2
3
4
5
6
7
import pytest

@pytest.fixture()
def database_environment():
    setup_database()
    yield
    teardown_database()

Všimnite si, že nastavenie databázy a jej zrušenie prebieha v tom istom prídavnom zariadení. Kľúčové slovo yield označuje časť, v ktorej pytest vykonáva skutočné testy.

Ak chcete, aby bol fixture skutočne použitý niektorým z vašich testov, stačí pridať názov fixture ako argument, napríklad takto (stále v súbore test_validator.py):

1
2
def test_world(database_environment):
    assert 1 == 1

Získavanie údajov zo zariadení

Namiesto použitia funkcie yield môže funkcia fixture vrátiť ľubovoľné hodnoty:

1
2
3
4
5
import pytest

@pytest.fixture()
def my_fruit():
    return "apple"

Opäť platí, že vyžiadanie tohto prípravku z testovacej funkcie sa vykoná zadaním názvu prípravku ako parametra:

1
2
def test_fruit(my_fruit):
    assert my_fruit == "apple"

Konfiguračné súbory

pytest môže čítať svoju konfiguráciu špecifickú pre projekt z jedného z týchto súborov:

  • pytest.ini
  • tox.ini
  • setup.cfg

Ktorý súbor použijete, závisí od toho, aké ďalšie nástroje môžete vo svojom projekte použiť. Ak ste svoj projekt zabalili, mali by ste použiť súbor setup.cfg. Ak používate tox na testovanie vášho kódu v rôznych prostrediach, môžete umiestniť konfiguráciu pytest do súboru tox.ini. Súbor pytest.ini sa môže použiť, ak nechcete využívať žiadne ďalšie nástroje, ale pytest.

Konfiguračný súbor vyzerá väčšinou rovnako pre každý z týchto troch typov súborov:

Používanie súborov pytest.ini a tox.ini

1
2
[pytest]
addopts = ​-rsxX -l --tb=short --strict​

Ak používate súbor setup.cfg, jediný rozdiel spočíva v tom, že sekciu [pytest] musíte predradiť slovom tool::

1
2
[tool:pytest]
addopts = ​-rsxX -l --tb=short --strict​

conftest.py

Každý priečinok obsahujúci testovacie súbory môže obsahovať súbor conftest.py, ktorý načíta pytest. Toto je dobré miesto na umiestnenie vlastných prípravkov, pretože by sa mohli zdieľať medzi rôznymi testovacími súbormi.

Súbor(-y) conftest.py môže(-ú) meniť správanie nástroja pytest na základe jednotlivých projektov.

Okrem zdieľaných fixtures by ste mohli umiestniť externé háčiky a zásuvné moduly alebo modifikátory pre PATH, ktoré používa pytest na zisťovanie testov a implementačného kódu.

CLI / PDB

Počas vývoja, hlavne keď píšete testy pred implementáciou, môže byť pytest užitočným nástrojom na ladenie.

Pozrieme sa na najužitočnejšie možnosti príkazového riadka.

Spustenie iba jedného testu

Ak chcete spustiť len jeden konkrétny test, môžete sa naň odvolať prostredníctvom súboru test_, v ktorom sa nachádza, a názvu funkcie:

1
pytest test_validator.py::test_regular_email_validates

Iba zber

Niekedy chcete mať k dispozícii len zoznam testovacej kolekcie, a nie vykonávať všetky testovacie funkcie.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
pytest --collect-only

============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 5 items

<Module test_validator.py>
  <Function test_regular_email_validates>
  <Function test_valid_email_has_one_at_sign>
  <Function test_valid_email_has_only_allowed_chars>
  <Function test_valid_email_can_have_plus_sign>
  <Function test_valid_email_must_have_a_tld>

========================== 5 tests collected in 0.01s ==========================

Ukončenie pri prvej chybe

Môžete prinútiť pytest, aby po neúspešnom teste zastavil vykonávanie ďalších testov:

1
pytest -x

Spustite iba posledný neúspešný test

Ak chcete spustiť len testy, ktoré naposledy zlyhali, môžete to urobiť pomocou príznaku --lf:

1
pytest --lf

Spustite všetky testy, ale najprv spustite posledné neúspešné testy

1
pytest --ff

Zobrazenie hodnôt lokálnych premenných vo výstupe

Ak nastavíme zložitejšiu testovaciu funkciu s niektorými lokálnymi premennými, môžeme príznakom -l dať príkaz pytest, aby tieto lokálne premenné zobrazil.

Prepíšeme našu testovaciu funkciu takto:

1
2
3
4
5
...
def test_valid_email_can_have_plus_sign():
    email = '[email protected]'
    assert is_valid_email_address('[email protected]')
...

Potom,

1
pytest -l

nám poskytne tento výstup:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example
collected 5 items

test_validator.py ...FF                                                  [100%]

=================================== FAILURES ===================================
_____________________ test_valid_email_can_have_plus_sign ______________________

    def test_valid_email_can_have_plus_sign():
        email = '[email protected]'
>       assert is_valid_email_address('[email protected]')
E       AssertionError: assert False
E        +  where False = is_valid_email_address('[email protected]')

email      = '[email protected]'

test_validator.py:18: AssertionError
_______________________test_valid_email_must_have_a_tld_______________________

    def test_valid_email_must_have_a_tld():
>       assert not is_valid_email_address('[email protected]')
E       AssertionError: assert not True
E        +  where True = is_valid_email_address('[email protected]')

test_validator.py:21: AssertionError
=========================== short test summary info ============================
FAILED test_validator.py::test_valid_email_can_have_plus_sign - AssertionErro...
FAILED test_validator.py::test_valid_email_must_have_a_tld - AssertionError: ...
========================= 2 failed, 3 passed in 0.09s ==========================

Používanie pytestu s debuggerom

V Pythone je zabudovaný debugger príkazového riadku s názvom pdb. Pomocou pytest môžete ladiť kód svojej testovacej funkcie.

Ak spustíte pytest s príkazom --pdb, spustí sa relácia ladenia pdb hneď po vyvolaní výnimky vo vašom teste. Väčšinou to nie je veľmi užitočné, pretože možno budete chcieť skontrolovať každý riadok kódu pred vyvolanou výnimkou.

Ďalšou možnosťou je príznak --trace pre pytest, ktorý nastaví bod prerušenia na prvom riadku každej funkcie testu. To sa môže stať trochu nepohodlné, ak máte veľa testov. Takže na účely ladenia je dobrá kombinácia --lf --trace, ktorá spustí reláciu ladenia s pdb na začiatku posledného testu, ktorý zlyhal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pytest --lf --trace

============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-7.0.1, pluggy-1.0.0
rootdir: /Users/bascodes/Code/blogworkspace/pytest-example, configfile: pytest.ini
collected 2 items
run-last-failure: rerun previous 2 failures

test_validator.py
>>>>>>>>>>>>>>>>>>>> PDB runcall (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
> /Users/bascodes/Code/blogworkspace/pytest-example/test_validator.py(17)test_valid_email_can_have_plus_sign()
-> email = '[email protected]'
(Pdb)

CI / CD

V moderných softvérových projektoch sa softvér vyvíja podľa princípov Test Driven Development a dodáva prostredníctvom kontinuálnej integrácie / kontinuálneho nasadenia, ktoré zahŕňa automatizované testovanie.

Typické nastavenie je také, že revízie do vetvy main/master sú zamietnuté, pokiaľ všetky testovacie funkcie neprejdú.

Ak sa chcete dozvedieť viac o používaní pytestu v prostredí CI/CD, zostaňte naladení, pretože na túto tému plánujem nový článok.

Dokumentácia

Oficiálna dokumentácia pre pytest je tu: https://docs.pytest.org

Krátka poznámka:

Toto je repost pôvodného článku od Bas Steins, ktorý bol vytvorený s jeho súhlasom. Ďalšie články nájdete na jeho stránke a/alebo ho sledujte na Twitteri: @bascodes