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 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 classe
  • teardown: eseguito una sola volta dopo ogni test in una classe
  • setup_class: eseguito una sola volta prima di tutti i test in una classe
  • teardown_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?