Classi e metodi di test

Completato

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 si vedrà in seguito, Pytest non interviene e consente di non forzare la scrittura dei test in un modo specifico.

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 in un determinato file è presente "sì" nel contenuto. In tal caso, restituisce True. Se il file non esiste o è presente "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ù facile 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 tutti i sistemi hanno una directory /tmp e tale percorso potrebbe non essere temporaneo a seconda del 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 non usano più i file dopo l'esecuzione. Prima di esaminare come sia possibile migliorarli, descriviamo i metodi helper nella sezione successiva.

Metodi di supporto

In una classe di test sono disponibili alcuni metodi che è possibile usare per 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: viene eseguito una volta prima di ogni test in una classe .
  • teardown: viene eseguito una volta dopo ogni test in una classe.
  • setup_class: viene eseguito una sola volta prima di tutti i test in una classe .
  • teardown_class: esegue una sola volta dopo tutti i test in una classe .

Quando i test richiedono risorse simili (o identiche), è utile scrivere i metodi di configurazione. 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

Diamo uno sguardo a 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 metodo teardown(), questa classe di test non lascia più indietro un /tmp/test_file.

Attrezzaggio

Un altro miglioramento che è possibile apportare a tale 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. L’esempio mostra come appare la classe con un metodo setup() aggiunto che dichiara la variabile di percorso:

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. Tali metodi non devono essere preceduti dal nome test e non possono essere denominati come metodi di installazione o pulizia. TestIsDone Nella classe è possibile automatizzare la creazione del file temporaneo in un 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 metodo write_tmp_file() e altri metodi possono chiamarlo direttamente per evitare attività ripetitive come la scrittura in un file.

L'intera classe si presenta come nell’esempio seguente, dopo aver aggiornato i metodi di test per utilizzare 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 con cui si lavora attualmente. Di seguito sono riportate alcune domande generali che consentono di determinare quando usare una classe:

  • I test richiedono un codice helper simile per la configurazione o la pulizia?
  • Il raggruppamento dei 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?