Parametrizzazione Pytest

Completato

La funzionalità di parametrizzazione in pytest potrebbe inizialmente sembrare complessa, ma lo scopo è semplice dopo aver compreso il problema che risolve. Essenzialmente, la parametrizzazione consente di eseguire la stessa funzione di test con input diversi in modo efficiente, semplificando l'esecuzione di asserzioni dettagliate e diverse con meno codice.

Quando si chiama parametrizza, il primo argomento è una stringa contenente uno o più nomi di argomento, "test\_input_"ad esempio . Il secondo argomento contiene un elenco di valori di argomento, ["27", "6+9", "0", "O"]ad esempio . Gli ultimi quattro argomenti hanno valori predefiniti e sono facoltativi.

È possibile trovare le informazioni di riferimento sull'API pytest in lingua inglese per parametrize qui: pytest.Metafunc.parametrize.

Quando usare la parametrizzazione

Due scenari comuni in cui è possibile usare parametrizza includono:

  • Quando si testano i cicli for
  • Quando più test assergono lo stesso comportamento

Esaminiamo prima di tutto ogni esempio senza usare parametrizza e quindi con esso per mostrare come può migliorare i test.

Cicli for

Di seguito è riportato un esempio di una funzione di test con un for ciclo:

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

Questo test è problematico perché, se non riesce, può causare diversi problemi, tra cui:

  • Report di test ambigui: Il report di test non chiarisce se un solo elemento non è riuscito o se sono presenti più errori.
  • Visualizzazione test singolo: Tutti gli elementi vengono considerati come un singolo test, che nasconde le prestazioni dei singoli elementi.
  • Correzioni incerte: Se un errore viene corretto, non è possibile sapere se tutti i problemi vengono risolti senza eseguire nuovamente l'intero test.

Modificare il test in modo da includere in modo specifico due elementi che dovrebbero avere esito negativo:

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

L'esecuzione del test mostra un solo errore anche se nell'elenco sono presenti due elementi non validi:

$ 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 ===============================

Questo è un ottimo caso d'uso per parametrizzare. Prima di poter vedere come aggiornare il test, si esaminerà un'altra situazione comune che non comporta for cicli.

Test che asseriscono lo stesso comportamento

Un gruppo di test che fanno la stessa asserzione è anche un buon candidato per la parametrizzazione. Se il test precedente è stato riscritto con un test per ogni elemento, ciò consentirebbe di segnalare meglio gli errori, ma sarebbe ripetitivo:

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

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

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

Questi test sono migliori nel senso che un errore può essere facilmente associato a un singolo input. Anche se potrebbe sembrare insolito avere diversi test simili, è comune vedere nei gruppi di test di produzione che tentano di essere granulari.

Anche se i test sono migliori perché possono segnalare esattamente ciò che ha esito negativo (o supera) presenta anche i problemi seguenti:

  • Il codice è ripetitivo, che crea un carico di manutenzione
  • Durante l'aggiornamento dei test è possibile che si verifichino errori e errori di digitazioni
  • Poiché sono ripetitivi, i tecnici potrebbero non includere tutti i casi d'uso e gli input

Come usare la parametrizzazione

Ora che si è a conoscenza di alcuni dei casi d'uso per la parametrizzazione, è possibile aggiornare il test che ha usato un ciclo for che include gli elementi con errori.

Per usare parametrize, è necessario importare pytest come libreria e quindi usarla come decoratore nella funzione. Ecco il test aggiornato:

import pytest

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

Prima di eseguire i test, esaminiamo le modifiche.

Il decoratore pytest.mark.parametrize() definisce due argomenti. Il primo argomento è una stringa denominata "item". Tale stringa viene usata come argomento denominato per la funzione di test visualizzata nella riga successiva nella definizione della funzione di test. Il secondo argomento è l'elenco dei valori di test.

Segnalazione errori avanzata

Dietro le quinte, pytest considera ogni elemento in tale elenco come test separato. Ciò significa che il superamento e l'esito negativo dei test vengono segnalati separatamente. Vediamo cosa accade quando si esegue il test con 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 ==========================

Nella creazione di report di test sono presenti alcuni elementi rilevanti. In primo luogo, si noterà che da un singolo test pytest vengono riportati cinque test in totale: tre superati e due falliti. Gli errori vengono segnalati separatamente, incluso l'input che ha esito negativo.

$ 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

È difficile non vedere il valore che ha causato l'errore, dato che viene segnalato in così tante posizioni.

Usare il flag di output dettagliato

Quando i test vengono eseguiti nella riga di comando, il report del superamento dei test è minima. Ecco come apparirà il test dopo un aggiornamento per risolvere i problemi:

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

L'esecuzione dei test produce un output minimo:

$ 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 ===============================

Aumentando il livello di dettaglio vengono visualizzati i valori eseguiti da pytest per ogni test quando si usa parametrizza:

$ 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 ===============================

Come usare più nomi di argomento

Gli esempi illustrati finora hanno solo un nome di argomento nel primo argomento. È stato usato "item" ma è possibile includere più nomi di argomento nella stringa che specifica il primo argomento separato da virgole.

Un caso d'uso per l'impiego di più nomi di argomento è quando si desidera fornire un set di valori attesi per confrontarli con il valore di input. Nel secondo argomento, ogni elemento del set deve avere una quantità di valori uguale al numero di nomi di input. Ad esempio, se i nomi di input sono "test\_input, expected\_value", il secondo argomento potrebbe essere simile al seguente: [("3+5", 8), ("3*4", 12)]

Questo test verifica se un oggetto ha un attributo usando la funzione Python hasattr() . Restituisce un valore booleano a seconda che l'oggetto abbia l'attributo associato.

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

Poiché hasattr() richiede due argomenti, è possibile usare parametrizza nel modo seguente:

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

Il decoratore parametrize utilizza ancora una singola stringa per il primo argomento, ma con due nomi di argomento separati da una virgola, che diventano argomenti per la funzione di test. In questo caso, è item e attribute.

Di seguito è riportato un elenco di due coppie di elementi. Ognuna di queste coppie rappresenta un oggetto item e un attribute oggetto per cui eseguire il test.

Quando pytest non riesce a creare una rappresentazione di stringa degli oggetti passati, ne crea uno. È possibile visualizzare questo risultato durante l'esecuzione del test:

$ 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 ===============================