Concetti di base su Pytest
Iniziamo a eseguire test con Pytest. Come accennato nell'unità precedente, Pytest è altamente configurabile e può gestire gruppi di test complessi, ma non richiede molto per iniziare a scrivere test. In effetti, è sempre preferibile che un framework consenta di scrivere test facilmente.
Al termine di questa sezione si dovrebbe avere a disposizione tutto il necessario 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 molte regole rigide relative ai file di test, alle directory di test o ai 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 al livello radice del progetto, ma non è insolito vederla insieme ai moduli di codice.
Nota
In questo modulo si useranno per impostazione predefinita i test 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 tests 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 invece sempre il plurale tests.
Testare le funzioni
Un argomento sicuro 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 prefisso test_ garantisce che Pytest raccolga il test e lo esegua.
Ecco come si presenta 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 verranno illustrate in modo più dettagliato in seguito, 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. Il test runner è un eseguibile nella riga di comando che (a livello generale) può:
- Eseguire la raccolta di test individuando tutti i file, le classi e le funzioni di test per eseguire un test.
- Avviare un'esecuzione di test eseguendo tutti i test.
- Tenere traccia di errori e di test superati.
- Fornire report avanzati al termine di un'esecuzione di test.
Nota
Poiché Pytest è una libreria esterna, deve essere installato per usarlo.
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 è presente il file test_main.py è possibile eseguire il file eseguibile pytest:
$ 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 chiamata assert. 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. Dietro le quinte, Pytest consente all'istruzione di eseguire confronti avanzati senza forzare l'utente a scrivere altro codice o configurare nulla.
Usando l'istruzione normaleassert, è possibile usare gli operatori python, ad 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 esamini 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
Pytest mostra un contesto utile intorno all'errore: un uso delle maiuscole e minuscole non corretto all'inizio della stringa e un carattere in più 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ò essere oneroso in caso di errori, ma Pytest si comporta in modo efficiente nel 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. Non solo questo report rileva e mostra l'errore, ma consente anche di apportare rapidamente modifiche per risolvere il problema.