Fixture di Pytest

Completato

Le fixture forniscono un contesto definito e affidabile per i test. Le fixture personalizzate sono funzioni Python decorate con @pytest.fixture. È possibile usarli per disporre precondizioni, ad esempio dati di test, variabili di ambiente, connessioni di database o file necessari a un test prima dell'esecuzione. Le fixture possono anche definire passaggi di smantellamento che ripuliscono le risorse dopo che l'ambito del test o della fixture è completato.

Un test richiede una fixture dichiarando un argomento con lo stesso nome della fixture. Pytest trova la fixture, la esegue e passa il valore restituito o prodotto al test. Per ulteriori informazioni sul ciclo di vita delle fixture, consultare la documentazione di pytest relativa alle fixture.

Le fixture sono più utili quando la configurazione o la pulizia viene riutilizzata, è complessa o più chiara quando viene assegnato il nome. Per semplici valori occasionali, mantenere il codice di configurazione direttamente nel test può risultare più facile da leggere.

Le caratteristiche principali delle fixture pytest includono:

  • Controllo ambito: le fixture possono usare il scope parametro per controllare la frequenza con cui pytest crea ed elimina definitivamente il valore della fixture. Ambiti più ampi possono migliorare le prestazioni, ma condividono anche lo stato per più tempo.
  • Configurazione e smantellamento: Le fixture possono usare yield per la pulizia. Le fixture avanzate possono anche ricevere la fixture integrata request (introdotta più avanti in questa unità) per analizzare il test richiedente e chiamare request.addfinalizer() per registrare i richiami di pulizia.
  • Inserimento delle dipendenze: i test e le fixture richiedono fixture come argomenti, rendendo esplicite le dipendenze.
  • Riutilizzabilità e modularità: le fixture possono essere definite una sola volta e riutilizzate tra funzioni di test, moduli, directory o progetti.
  • Attivazione automatica: una fixture definita con @pytest.fixture(autouse=True) viene eseguita per ogni test nel suo ambito di disponibilità senza dover essere elencata come argomento. Usare questa opzione con moderazione perché nasconde le dipendenze del test.

Creazione di una fixture di file temporanea

Quando si scrivono test che interagiscono con i file, è comune avere percorsi isolati che non entrano in conflitto con altri test o dipendono da una posizione predefinita. Pytest fornisce fixture di directory temporanee integrate che sono in genere la scelta migliore per i nuovi test perché pytest crea directory univoche e ne gestisce la conservazione. Tuttavia, la creazione di una piccola fixture personalizzata è un modo utile per comprendere le richieste delle fixture e i valori restituiti.

La fixture seguente restituisce una funzione factory. La fixture stessa non crea un file durante l'installazione. Al contrario, la funzione nidificata create() crea un file quando il test chiama tmp_file():

from pathlib import Path
import tempfile

import pytest


@pytest.fixture
def tmp_file():
    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        path.write_text(contents, encoding="utf-8")
        return path

    return create

La fixture usa la funzione tempfile.NamedTemporaryFile() di Python con delete=False in modo che il file esista ancora dopo la chiusura. L'handle di file viene chiuso prima che il test riceva il percorso, cosa importante quando i test devono riaprire o eliminare il file, soprattutto su Windows.

Ecco un esempio di come un test può usare la fixture:

def test_file(tmp_file):
    path = tmp_file("ready")
    assert path.read_text(encoding="utf-8") == "ready"

In questa configurazione, tmp_file è il nome della fixture e il test riceve la create() funzione restituita dalla fixture. La chiamata tmp_file() all'interno del test crea il file, scrive il contenuto richiesto e restituisce un pathlib.Path oggetto per esso.

Note

Prestare attenzione quando si combinano le fixture con i gestori del contesto. Se una fixture restituisce un percorso o un handle dall'interno di un with blocco, il gestore del contesto potrebbe chiudere o eliminare la risorsa prima che il test lo usi. Mantenere aperto il gestore del contesto in un yield quando il test richiede la risorsa gestita o chiudere intenzionalmente la risorsa prima di restituirla e pulirla in un secondo momento.

Gestione dell'ambito

Per impostazione predefinita, le fixture pytest usano l'ambito function. Una fixture con scopo di funzione viene creata quando un test la richiede e viene distrutta alla fine di quel test. Se la fixture usa il codice di disinstallazione, pytest esegue il codice di disinstallazione alla fine dell'ambito della fixture.

Pytest consente anche ad ambiti di fixture più ampi di ottimizzare il lavoro di configurazione oneroso. I nomi di ambito della fixture predefiniti sono:

  • function: ambito predefinito. La fixture viene creata una volta per ogni funzione di test.
  • class: La fixture viene creata una sola volta per ogni classe di test.
  • module: la fixture viene creata una volta per il modulo.
  • package: la fixture viene creata una sola volta per il pacchetto in cui viene definita e viene eliminata dopo l'ultimo test in tale pacchetto, inclusi i test nei sottopacchetti e nelle sottodirectory.
  • session: la fixture viene creata una volta per la sessione di test.

Per i casi avanzati, scope può anche essere un chiamabile che restituisce uno di questi nomi di ambito. Pytest chiama la funzione una volta quando definisce la fixture e passa gli argomenti fixture_name e config della parola chiave. Consultare la documentazione di pytest per informazioni dettagliate sull'ambito dinamico.

In questo contesto, creato una sola volta significa che pytest memorizza il valore della fixture per tale ambito. Ad esempio, una fixture con ambito modulo può essere richiesta da diversi test in un modulo, ma questi test ricevono lo stesso valore della fixture memorizzata nella cache. Una fixture con parametri è l'eccezione: pytest può creare un nuovo valore per ogni set di parametri all'interno dell'ambito.

Mantenere le fixture con ambito di funzione, a meno che la condivisione delle configurazioni non sia sicura e utile. Gli oggetti modificabili, le connessioni aperte, le modifiche dell'ambiente e i file possono perdere lo stato tra i test quando vengono memorizzati nella cache da un ambito più ampio.

Ecco l'aspetto della fixture tmp_file con un ambito di modulo:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture(scope="module")
def tmp_file():
    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        path.write_text(contents, encoding="utf-8")
        return path

    return create

Poiché questa fixture restituisce una funzione factory, scope="module" memorizza nella cache la funzione factory per il modulo. La factory crea comunque un nuovo file ogni volta che un test chiama tmp_file(). Se la fixture ha creato e restituito direttamente un percorso di file, ogni test nel modulo riceverà lo stesso percorso.

Gestione della pulizia

Le versioni precedenti della tmp_file fixture creano file temporanei, ma non vengono rimossi. Pytest consiglia yield fixture per una pulizia semplice. Il codice prima di yield esegue l'installazione e fornisce un valore al test. Codice dopo che yield viene eseguito durante la rimozione, anche quando il test fallisce dopo il completamento della configurazione. Se una yield fixture si attiva prima che raggiunga yield, pytest non esegue il codice di teardown successivamente a tale yield, ma rimuove comunque tutte le fixture precedenti completate correttamente per lo stesso test.

La versione seguente tiene traccia di ogni file creato dalla factory e tenta di eliminare tali file al termine del test:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture
def tmp_file():
    paths = []

    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        paths.append(path)
        path.write_text(contents, encoding="utf-8")
        return path

    yield create

    for path in paths:
        try:
            path.unlink()
        except FileNotFoundError:
            pass

Con l'ambito predefinito function , pytest esegue il codice di pulizia dopo ogni test che richiede tmp_file. Se si modifica la fixture in scope="module", pytest esegue la pulizia durante lo smantellamento del modulo, dopo che pytest ha finito di eseguire i test in quel modulo. Quando diverse yield fixture sono attive, pytest esegue il codice di disinstallazione in ordine inverso dell'installazione.

La pulizia usa try/except FileNotFoundError perché un test potrebbe eliminare il file stesso. In Windows, l'eliminazione può anche non riuscire con PermissionError se un test ha lasciato aperto il file, quindi assicurarsi che i test chiudano eventuali handle creati. È anche possibile usare request.addfinalizer() per la pulizia quando è necessario un maggiore controllo. Registrare il finalizzatore solo dopo la creazione della risorsa, perché pytest esegue un finalizzatore aggiunto durante la disinstallazione anche se la fixture viene generata in un secondo momento. Per una pulizia semplice, yield le fixture sono in genere più facili da leggere.

Uso di conftest.py

Anziché definire le fixture in ogni file di test, è possibile salvare le fixture condivise in un file denominato conftest.py. Le fixture in conftest.py sono automaticamente disponibili per i test nella stessa directory e nelle relative sottodirectory senza importazioni esplicite.

È possibile avere diversi conftest.py file in un gruppo di test. Pytest determina la disponibilità della fixture dal punto di vista del test che la richiede: le fixture nella classe o nel modulo del test vengono considerate nell'ambito locale, quindi le fixture dai file conftest.py nella stessa directory e nelle directory superiori. I test possono cercare le fixture verso l'alto, ma non cercano verso il basso in directory figlio o di pari livello. Le fixture dei plug-in vengono cercate dopo le fixture locali. Non importare da conftest.py nei file di test. Lasciare che pytest lo rilevi.

Esplorazione delle fixture predefinite

Pytest include molte fixture predefinite progettate per semplificare i test. Queste fixture gestiscono attività comuni di installazione e pulizia in modo che sia possibile concentrarsi sulla scrittura di asserzioni di test. Per esaminare l'elenco completo, vedere le informazioni di riferimento su pytest per le fixture predefinite.

Gli elementi principali integrati includono:

  • cache: archivia e recupera i valori nelle esecuzioni pytest.
  • capsys: acquisisce il testo scritto in stdout e stderr.
  • tmp_path: fornisce un pathlib.Path oggetto per una directory temporanea univoca per ogni chiamata di funzione di test, incluso ogni caso parametrizzato.
  • tmpdir: fornisce una directory temporanea come oggetto py.path.local legacy. Per i nuovi test, pytest consiglia tmp_path.
  • monkeypatch: modifica temporaneamente classi, funzioni, dizionari, variabili di ambiente e altri oggetti.
  • request: fornisce informazioni sul test o fixture attualmente in esecuzione e supporta i finalizzatori.

Altre fixture predefinite comunemente usate includono caplog (acquisisce i record di log), capfd (acquisisce l'output a livello di descrittore di file), tmp_path_factory (crea directory temporanee con ambito sessione) e pytestconfig (espone la configurazione pytest attiva). Per l'elenco completo, consultare il riferimento su pytest per le fixture integrate.

L'esercizio successivo usa tmp_path e pathlib.Path per creare file temporanei in una directory per test. È possibile riscontrare tmpdir nei progetti pytest esistenti, ma per il nuovo codice preferire tmp_path. Per impostazione predefinita, pytest mantiene le directory temporanee delle ultime esecuzioni di test, quindi la visualizzazione delle cartelle temporanee pytest-managed recenti dopo un'esecuzione è prevista. Per altre informazioni, vedere la documentazione di pytest per directory e file temporanei.

Il ruolo di monkeypatching nei test

Le configurazioni di test precedenti hanno creato e pulito le risorse per il test. Un altro uso comune delle fixture è la modifica temporanea dello stato nei test che devono isolare il codice dallo stato esterno. Per questi casi, pytest fornisce la fixture predefinita monkeypatch , che è essa stessa una fixture pytest e un buon esempio di disinstallazione automatica.

Il test del codice che dipende da risorse esterne, configurazione globale o stato del sistema operativo può risultare complesso. La fixture monkeypatch consente di modificare temporaneamente attributi, valori di dizionario, variabili di ambiente, la directory corrente di lavoro o sys.path per un test. Pytest annulla automaticamente tali modifiche una volta che la funzione di test o la fixture è completata.

Di seguito è riportato un esempio che sostituisce un attributo di funzione in un piccolo modulo di progetto per un test. L'applicazione di patch al codice dell'applicazione con ambito ristretto (anziché le funzioni della libreria standard usate da pytest stesso) mantiene il test focalizzato ed evita di rompere accidentalmente il comportamento di pytest stesso.

# project_module.py
def get_user_id():
    # Imagine this calls a remote service.
    return "real-user"
# test_project.py
import project_module


def test_user_id(monkeypatch):
    monkeypatch.setattr(project_module, "get_user_id", lambda: "test-user")
    assert project_module.get_user_id() == "test-user"

È anche possibile specificare la destinazione come stringa del percorso di importazione punteggiata. Pytest risolve e importa il modulo denominato quando applica la patch, utile quando il file di test non richiede già il modulo. Successivamente import project_module in questo esempio serve solo a far sì che il test possa fare riferimento alla funzione modificata per l'asserzione:

# test_project.py
def test_user_id_by_path(monkeypatch):
    monkeypatch.setattr("project_module.get_user_id", lambda: "test-user")

    import project_module
    assert project_module.get_user_id() == "test-user"

A parte l'impostazione degli attributi e l'override dei metodi con setattr(), la fixture monkeypatch può eliminare gli attributi con delattr(), impostare ed eliminare variabili dell'ambiente (setenv, delenv), impostare ed eliminare elementi del dizionario (setitem, delitem), anteporre sys.path (syspath_prepend), modificare la directory di lavoro corrente (chdir) e creare un ambito di patch annidato con context(). Il comportamento predefinito raising=True per setattr() e delattr() consente di rilevare destinazioni con errori di ortografia. Usare monkeypatch.context() quando una patch deve essere annullata prima che il resto di un test o lo smantellamento della fixture continui. Per ulteriori informazioni, consultare la documentazione di pytest per monkeypatching.

Anche se monkeypatch è potente, usarlo con attenzione:

  • Chiarezza e manutenzione del codice: l'utilizzo monkeypatch eccessivo o l'uso in modi complessi può rendere i test più difficili da comprendere e gestire.
  • Validità dei test: monkeypatching può creare test che superano condizioni artificiali molto diverse dal comportamento di produzione.
  • Eccessiva dipendenza dai dettagli dell'implementazione: i test che modificano i dettagli dell'implementazione interna potrebbero risultare instabili quando il codice sottostante cambia.
  • Complessità del debug: il debug di test che usano monkeypatch può essere più difficile quando una patch modifica il comportamento fondamentale dell'applicazione.

Usare monkeypatch quando consente di isolare un comportamento mirato e mantenere ogni patch il più piccolo e esplicito possibile.