Pytest parametrize

Fuldført

Den parametrize funktion i pytest kan i første omgang synes kompleks, men dens formål er ligetil, når du forstår det problem, det løser. Grundlæggende giver parametrize dig mulighed for at køre den samme testfunktion med forskellige input effektivt, hvilket gør det nemmere at køre detaljerede og varierede antagelser med mindre kode.

Når du kalder parametrize, er det første argument en streng, der indeholder et eller flere argumentnavne, f.eks. "test\_input_". Det andet argument indeholder en liste over argumentværdier, f.eks. ["27", "6+9", "0", "O"]. De sidste fire argumenter har standardværdier og er valgfrie.

Du kan finde API-referencen til pytest på engelsk for parametrize her: pytest. Metafunc.parametrize.

Hvornår skal du bruge parametrize?

To almindelige scenarier, hvor du måske vil bruge parametrize omfatter:

  • Når du tester for løkker
  • Når flere test hævder den samme funktionsmåde

Lad os først gennemgå hvert eksempel uden at bruge parametrizeog derefter med det for at vise, hvordan det kan forbedre vores test.

Til løkker

Her er et eksempel på en testfunktion med en for-løkke:

def test_string_is_digit():
    items = ["1", "10", "33"]
    for item in items:
        assert item.isdigit()

Denne test er problematisk, fordi hvis den mislykkes, kan det føre til flere problemer, herunder:

  • flertydige testrapporter: Testrapporten præciserer ikke, om kun ét element mislykkedes, eller om der er flere fejl.
  • Enkelt testvisning: Alle elementer ses som en enkelt test, hvilket skjuler ydeevnen for individuelle elementer.
  • Usikre rettelser: Hvis en fejl rettes, er der ingen måde at vide, om alle problemer er løst uden at køre hele testen igen.

Lad os ændre testen, så den specifikt indeholder to elementer, der forventes at mislykkes:

def test_string_is_digit():
    items = ["No", "1", "10", "33", "Yes"]
    for item in items:
        assert item.isdigit()

Hvis du kører testen, vises der kun én fejl, selvom der er to ugyldige elementer på listen:

$ pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________
test_items.py:4: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x103fa1df0>()
E    +    where <built-in method isdigit of str object at 0x103fa1df0> = 'No'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================

Dette er en god brugssag for parametrize. Før vi kan se, hvordan vi opdaterer testen, kan vi udforske en anden almindelig situation, der ikke involverer for løkker.

Test, der hævder den samme funktionsmåde

En gruppe test, der foretager den samme antagelse, er også en god kandidat til parametrize. Hvis den forrige test blev omskrevet med én test for hvert element, ville det give mulighed for bedre fejlrapportering, men det ville være gentagende:

def test_is_digit_1():
    assert "1".isdigit()

def test_is_digit_10():
    assert "10".isdigit()

def test_is_digit_33():
    assert "33".isdigit()

Disse test er bedre i den forstand, at en fejl nemt kan knyttes til et enkelt input. Og selvom det kan synes usædvanligt at have flere lignende test, er det almindeligt at se i produktionstestpakker, der forsøger at være kornet.

Selvom testene ville være bedre, fordi de kan rapportere nøjagtigt, hvad der mislykkes (eller består), kommer de også med følgende problemer:

  • Koden er gentagen, hvilket skaber en vedligeholdelsesbyrde
  • Der er potentiale for stavefejl og fejl, når testene opdateres
  • Da de er gentagne, inkluderer teknikere muligvis ikke alle use cases og input

Sådan bruger du parametrize

Nu, hvor du er opmærksom på nogle af use cases for parametrize, kan vi opdatere den test, der brugte en for løkke, der omfatter elementer, der ikke fungerer.

Hvis du vil bruge parametrize, skal du importere pytest som et bibliotek og derefter bruge det som dekoratør i funktionen . Her er den opdaterede test:

import pytest

@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
    assert item.isdigit()

Før vi kører testene, gennemgår vi ændringerne.

Den pytest.mark.parametrize() dekoratør definerer to argumenter. Det første argument er en streng, der kaldes "item". Denne streng bruges som det navngivne argument for den testfunktion, der vises på den næste linje i definitionen af testfunktionen. Det andet argument er listen over testværdier.

Omfattende fejlrapportering

I baggrunden betragter pytest hvert element på listen som en separat test. Det betyder, at beståelses- og mislykket test rapporteres separat. Lad os se, hvad der sker, når du kører testen med pytest:

$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

test_items.py F...F                                                      [100%]

=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45e30>()
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
__________________________ test_string_is_digit[Yes] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45df0>()
E    +    where <built-in method isdigit of str object at 0x102d45df0> = 'Yes'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================

Der er nogle få bemærkelsesværdige elementer i testrapportering. For det første kan vi se, at fra en enkelt test-pytest rapporterer fem test i alt: tre afleveringer og to fejl. Fejlene rapporteres separat, herunder hvad det fejlende input er.

$ pytest test_items.py
___________________________ test_string_is_digit[No] ___________________________
[...]
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False

Det er svært at gå glip af den værdi, der forårsagede fejlen, med så mange steder, hvor den bliver rapporteret.

Brug det detaljerede outputflag

Når testene køres på kommandolinjen, er testrapportering, når test bestås, minimal. Sådan ser testen ud efter en opdatering for at løse fejlene:

@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
    assert item.isdigit()

Og kørsel af testene giver minimalt output:

$ pytest test_items.py 
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

test_items.py .....                                                      [100%]

============================== 5 passed in 0.01s ===============================

Hvis du øger verbositeten, vises de værdier, som pytest kører for hver test, når der bruges parametrize:

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
collected 5 items

test_items.py::test_string_is_digit[0] PASSED                            [ 20%]
test_items.py::test_string_is_digit[1] PASSED                            [ 40%]
test_items.py::test_string_is_digit[10] PASSED                           [ 60%]
test_items.py::test_string_is_digit[33] PASSED                           [ 80%]
test_items.py::test_string_is_digit[9] PASSED                            [100%]

============================== 5 passed in 0.01s ===============================

Sådan bruges flere argumentnavne

De eksempler, vi har set indtil videre, har kun ét argumentnavn i det første argument. Vi har brugt "item" men du kan medtage flere argumentnavne i strengen, der angiver det første argument adskilt af kommaer.

En use case til brug af flere argumentnavne er, hvis du vil overføre et sæt forventede værdier, der skal testes i forhold til din inputværdi. I det andet argument skal hvert element i sættet have en mængde værdier, der svarer til antallet af inputnavne. Hvis dine inputnavne f.eks. er "test\_input, expected\_value", kan dit andet argument se nogenlunde sådan ud: [("3+5", 8), ("3*4", 12)]

Denne test kontrollerer, om et objekt har en attribut ved hjælp af funktionen Python hasattr(). Den returnerer en boolesk værdi, afhængigt af om objektet har den tilknyttede attribut.

>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False

Da hasattr() kræver to argumenter, kan vi bruge parametrize på følgende måde:

@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

Den parametrize dekoratøren bruger stadig en enkelt streng til det første argument, men med to argumentnavne adskilt af et komma, som bliver argumenter til testfunktionen. I dette tilfælde er det item og attribute.

Næste er en liste over to par elementer. Hvert af disse par repræsenterer et item og et attribute, der skal testes for.

Når pytest ikke kan oprette en strengrepræsentation af de objekter, der overføres, oprettes der en. Du kan se dette, når du kører testen:

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
collected 2 items

test_items.py::test_attributes[-format] PASSED                           [ 50%]
test_items.py::test_attributes[item1-append] PASSED                      [100%]

============================== 2 passed in 0.01s ===============================