Classi e metodi di test
Oltre a scrivere funzioni di test, Pytest consente di usare le classi. Come già accennato, non c'è bisogno di ereditarietà e le classi di test seguono alcune semplici regole. L'uso delle classi offre flessibilità e possibilità di riutilizzo maggiori. Come illustrato di seguito, Pytest eviterà di scrivere i test in un certo modo.
In modo analogo alle funzioni, è comunque possibile scrivere asserzioni tramite l'istruzione assert
.
Creare una classe di test
Per comprendere il modo in cui le classi di test possono essere utili, si userà uno scenario reale. La funzione seguente controlla se un determinato file contiene "sì" nel relativo contenuto. In tal caso, restituisce True
. Se il file non esiste o se contiene "no" nel contenuto, restituisce False
. Questo scenario è comune nelle attività asincrone che usano il file system per indicare il completamento.
Ecco come appare la funzione:
import os
def is_done(path):
if not os.path.exists(path):
return False
with open(path) as _f:
contents = _f.read()
if "yes" in contents.lower():
return True
elif "no" in contents.lower():
return False
Ecco ora come appare una classe con due test (uno per ogni condizione) in un file denominato test_files.py :
class TestIsDone:
def test_yes(self):
with open("/tmp/test_file", "w") as _f:
_f.write("yes")
assert is_done("/tmp/test_file") is True
def test_no(self):
with open("/tmp/test_file", "w") as _f:
_f.write("no")
assert is_done("/tmp/test_file") is False
Attenzione
I metodi di test usano il percorso /tmp per un file di test temporaneo perché è più semplice da usare per l'esempio. Se è necessario usare file temporanei, tuttavia, prendere in considerazione l'uso di una libreria come tempfile
, che può crearli (e rimuoverli) in modo sicuro. Non in tutti i sistemi è presente una directory /tmp e tale percorso può non essere temporaneo in base al sistema operativo.
L'esecuzione dei test con il flag -v
per aumentare il livello di dettaglio mostra i test superati:
pytest -v test_files.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
cachedir: .pytest_cache
rootdir: /private/
collected 2 items
test_files.py::TestIsDone::test_yes PASSED [ 50%]
test_files.py::TestIsDone::test_no PASSED [100%]
============================== 2 passed in 0.00s ===============================
Anche se i test vengono superati, sembrano ripetitivi e lasciano anche i file al termine del test. Prima di vedere come migliorarle, verranno illustrati i metodi helper nella sezione successiva.
Metodi di supporto
In una classe di test sono disponibili alcuni metodi che consentono di configurare e rimuovere l'esecuzione dei test. Pytest li esegue automaticamente se sono definiti. Per usare tali metodi, è necessario sapere che sono associati a un ordine e a un comportamento specifici.
setup
: eseguito una sola volta prima di ogni test in una classeteardown
: eseguito una sola volta dopo ogni test in una classesetup_class
: eseguito una sola volta prima di tutti i test in una classeteardown_class
: eseguito una sola volta dopo tutti i test in una classe
Quando i test richiedono risorse simili (o identiche), è utile scrivere metodi di installazione. Idealmente, un test non deve lasciare le risorse dopo che è stato completato, quindi i metodi di rimozione sono utili nella pulizia dei test in tali situazioni.
Pulizia
Si esaminerà ora una classe di test aggiornata che pulisce i file dopo ogni test:
class TestIsDone:
def teardown(self):
if os.path.exists("/tmp/test_file"):
os.remove("/tmp/test_file")
def test_yes(self):
with open("/tmp/test_file", "w") as _f:
_f.write("yes")
assert is_done("/tmp/test_file") is True
def test_no(self):
with open("/tmp/test_file", "w") as _f:
_f.write("no")
assert is_done("/tmp/test_file") is False
Poiché è stato usato il teardown()
metodo , questa classe di test non lascia più indietro / tmp/test_file .
Impostazione
Un altro miglioramento che è possibile apportare a questa classe consiste nell'aggiungere una variabile che punta al file. Poiché il file è ora dichiarato in sei posizioni, qualsiasi modifica al percorso determinerebbe una modifica in tutte le posizioni. Questo esempio mostra l'aspetto della classe con un metodo aggiunto setup()
che dichiara la variabile path:
class TestIsDone:
def setup(self):
self.tmp_file = "/tmp/test_file"
def teardown(self):
if os.path.exists(self.tmp_file):
os.remove(self.tmp_file)
def test_yes(self):
with open(self.tmp_file, "w") as _f:
_f.write("yes")
assert is_done(self.tmp_file) is True
def test_no(self):
with open(self.tmp_file, "w") as _f:
_f.write("no")
assert is_done(self.tmp_file) is False
Metodi helper personalizzati
È possibile creare metodi helper personalizzati in una classe. Questi metodi non devono essere preceduti dal nome test
e non possono essere denominati come metodi di installazione o pulizia. Nella classe TestIsDone
è possibile automatizzare la creazione del file temporaneo in un metodo helper personalizzato. Questo metodo helper personalizzato potrebbe essere simile all'esempio seguente:
def write_tmp_file(self, content):
with open(self.tmp_file, "w") as _f:
_f.write(content)
Pytest non esegue automaticamente il write_tmp_file()
metodo e altri metodi possono chiamarlo direttamente per salvare attività ripetitive come la scrittura in un file.
L'intera classe è simile a questo esempio, dopo aver aggiornato i metodi di test per usare l'helper personalizzato:
class TestIsDone:
def setup(self):
self.tmp_file = "/tmp/test_file"
def teardown(self):
if os.path.exists(self.tmp_file):
os.remove(self.tmp_file)
def write_tmp_file(self, content):
with open(self.tmp_file, "w") as _f:
_f.write(content)
def test_yes(self):
self.write_tmp_file("yes")
assert is_done(self.tmp_file) is True
def test_no(self):
self.write_tmp_file("no")
assert is_done(self.tmp_file) is False
Uso di una classe anziché di una funzione
Non esistono regole rigorose sull'uso di una classe anziché di una funzione. È sempre consigliabile seguire le convenzioni dei progetti e dei team attuali con cui si lavora. Di seguito sono riportate alcune domande generali che consentono di determinare quando usare una classe:
- I test richiedono codice helper di installazione o pulizia simile?
- Raggruppare i test ha senso logico?
- Ci sono almeno alcuni test nel gruppo di test?
- I test possono trarre vantaggio da un set comune di funzioni helper?