Concetti di base su Pytest

Completato

Iniziamo a eseguire test con Pytest. Come accennato nell'unità precedente, Pytest è altamente configurabile e può gestire gruppi di test complessi. Non richiede tuttavia molto tempo per iniziare a scrivere test. In effetti, è sempre preferibile che un framework consenta di scrivere test facilmente.

Alla fine di questa sezione è necessario avere tutti gli elementi necessari per iniziare a scrivere i primi test ed eseguirli con Pytest.

Convenzioni

Prima di approfondire la scrittura dei test, è necessario esaminare alcune convenzioni di test su cui si basa Pytest.

Non esistono regole rigide sui file, sulle directory o sui layout di test generali in Python. Se si conoscono queste regole, è possibile sfruttare l'individuazione e l'esecuzione automatiche dei test senza la necessità di alcuna configurazione aggiuntiva.

Directory e file di test

La directory principale per i test è la directory tests. È possibile posizionare questa directory a livello radice del progetto, ma non è insolito vederla insieme ai moduli di codice.

Nota

Per impostazione predefinita, in questo modulo si userà la directory tests nella radice di un progetto.

Esaminiamo come appare la radice di un progetto Python di piccole dimensioni denominato jformat :

.
├── README.md
├── jformat
│   ├── __init__.py
│   └── main.py
├── setup.py
└── tests
    └── test_main.py

La directory tests si trova nella radice del progetto con un singolo file di test. In questo caso, il file di test è denominato test_main.py. Questo esempio illustra due convenzioni critiche:

  • Usare una directory dei test per inserire file di test e directory di test annidate.
  • Aggiungere un prefisso test ai file di test. Il prefisso indica che il file contiene codice di test.

Attenzione

Evitare di usare test (forma singolare) come nome della directory. Il nome test è un modulo Python, quindi la creazione di una directory con lo stesso nome ne provoca la sostituzione. Usare sempre il plurale tests .

Testare le funzioni

Uno degli argomenti sicuri per l'uso di Pytest è che consente di scrivere funzioni di test. Analogamente ai file di test, le funzioni di test devono essere precedute da test_. Il test_ prefisso garantisce che Pytest raccolga il test ed esegue il test.

Ecco l'aspetto di una semplice funzione di test:

def test_main():
    assert "a string value" == "a string value"

Nota

Se si ha familiarità con unittest, potrebbe essere sorprendente vedere l'uso di assert nella funzione di test. Le asserzioni semplici vengono illustrate in modo più dettagliato in un secondo momento, ma con Pytest si ottengono segnalazioni di errori avanzate con asserzioni semplici.

Classi e metodi di test

Analogamente alle convenzioni per file e funzioni, le classi di test e i metodi di test usano le convenzioni seguenti:

  • Le classi di test sono precedute dal prefisso Test
  • I metodi di test sono preceduti dal prefisso test_

Una differenza fondamentale con la libreria di unittest Python è che non è necessaria l'ereditarietà.

L'esempio seguente usa questi prefissi e altre convenzioni di denominazione Python per classi e metodi. Illustra una piccola classe di test che controlla i nomi utente in un'applicazione.

class TestUser:

    def test_username(self):
        assert default() == "default username"

Esecuzione dei test

Pytest è sia un framework di test che uno strumento di esecuzione di test. Lo strumento di esecuzione di test è un eseguibile nella riga di comando che ad alto livello può:

  • Eseguire la raccolta di test trovando tutti i file di test, le classi di test e le funzioni di test per un'esecuzione di test.
  • Avviare un'esecuzione di test eseguendo tutti i test.
  • Tenere traccia di errori, errori e superamento dei test.
  • Fornire report avanzati alla fine di un'esecuzione di test.

Nota

Poiché Pytest è una libreria esterna, è necessario installarla per usarla.

Dati questi contenuti in un file test_main.py, è possibile osservare il comportamento di Pytest durante l'esecuzione dei test:

# contents of test_main.py file

def test_main():
    assert True

Nella riga di comando, nello stesso percorso in cui esiste il file test_main.py , è possibile eseguire il pytest file eseguibile:

 $ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item

test_main.py .                                                       [100%]

============================ 1 passed in 0.00s =============================

In background, Pytest raccoglie il test di esempio nel file di test senza alcuna configurazione necessaria.

Istruzione di asserzione potente

Finora, tutti gli esempi di test usano la semplice assert chiamata. In genere, in Python, l'istruzione assert non viene usata per i test perché non dispone di report appropriati quando l'asserzione non riesce. Pytest, tuttavia, non ha questa limitazione. In background, Pytest consente all'istruzione di eseguire confronti avanzati senza forzare l'utente a scrivere altro codice né a configurare alcun elemento.

Con l'istruzione normale assert, è possibile usare gli operatori di Python. Esempio: >, <, !=, >= o <=. Tutti gli operatori di Python sono validi. Questa funzionalità potrebbe essere quella cruciale di Pytest: non è necessario apprendere una nuova sintassi per scrivere asserzioni.

Esaminiamo cosa comporta quando si gestiscono confronti comuni con oggetti Python. In questo caso, si esaminerà il report degli errori durante il confronto di stringhe lunghe:

================================= FAILURES =================================
____________________________ test_long_strings _____________________________

    def test_long_strings():
        left = "this is a very long strings to be compared with another long string"
        right = "This is a very long string to be compared with another long string"
>       assert left == right
E       AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E         - This is a very long string to be compared with another long string
E         ? ^
E         + this is a very long strings to be compared with another long string
E         ? ^                         +

test_main.py:4: AssertionError

In relazione all'errore, Pytest mostra un contesto molto utile. Maiuscole e minuscole non corrette all'inizio della stringa e un carattere aggiuntivo in una parola. Oltre alle stringhe, Pytest può essere utile con altri oggetti e strutture dei dati. Ad esempio, ecco come si comporta con gli elenchi:

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E         At index 1 diff: 'wheat' != 'coffee'
E         Full diff:
E         - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E         ?                     ---------
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?           +++++++++

test_main.py:9: AssertionError

Il report identifica che l'indice 1 (secondo elemento nell'elenco) è diverso. Non solo identifica il numero di indice, ma fornisce anche una rappresentazione dell'errore. Oltre ai confronti degli elementi, può anche segnalare se mancano elementi e fornire informazioni in grado di indicare esattamente quale può essere l'elemento mancante. Nel caso seguente:"milk"

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E         At index 2 diff: 'coffee' != 'salt'
E         Left contains one more item: 'milk'
E         Full diff:
E         - ['sugar', 'wheat', 'salt', 'water', 'milk']
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?                    ++++++++++

test_main.py:9: AssertionError

Vediamo infine il comportamento con i dizionari. Il confronto di due dizionari di grandi dimensioni può risultare eccessivo in caso di errori, ma Pytest esegue un processo in sospeso per fornire contesto e individuare l'errore:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         {'number': 39} != {'number': 38}
E         Full diff:
E           {
E            'county': 'Frett',...
E
E         ...Full output truncated (12 lines hidden), use '-vv' to show

In questo test sono presenti due errori nel dizionario. Il primo errore consiste nel fatto che il valore "street" è diverso, mentre l'altro consiste nella mancata corrispondenza di "number".

Pytest rileva accuratamente queste differenze (anche se si tratta di un errore in un singolo test). Poiché i dizionari contengono molti elementi, Pytest omette le parti identiche e mostra solo il contenuto pertinente. Vediamo cosa accade se si usa il flag suggerito -vv per aumentare il livello di dettaglio nell'output:

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E         Common items:
E         {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E         Differing items:
E         {'number': 39} != {'number': 38}
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         Full diff:
E           {
E            'county': 'Frett',
E         -  'number': 38,
E         ?             ^
E         +  'number': 39,
E         ?             ^
E            'state': 'Nevada',
E         -  'street': 'Ferry Lane',
E         ?                    - ^
E         +  'street': 'Ferry Ln.',
E         ?                     ^
E            'zipcode': 30877,
E           }

Se si esegue pytest -vv, il report aumenta il livello di dettaglio e fornisce un confronto granulare. Questo report non solo rileva e mostra l'errore, ma consente di apportare rapidamente modifiche per risolvere il problema.