Come eseguire test di unità in Flask

Introduzione

La testatura è essenziale nel processo di sviluppo del software, garantendo che il codice si comporti come previsto e sia privo di errori. In Python, pytest è un popolare framework di testatura che offre svariati vantaggi rispetto al modulo standard unit test, che è un framework di testatura integrato di Python e fa parte della libreria standard. pytest include una sintassi più semplice, un output migliore, fixture potenti e un’ecosistema plugin ricco. Questo tutorial vi guiderà attraverso la configurazione di un’applicazione Flask, l’integrazione di fixture di pytest e la scrittura di test unitarie utilizzando pytest.

Prerequisiti

Prima di iniziare, avrete bisogno di ciò seguente:

  • Un server in esecuzione su Ubuntu e un utente non root con privilegi di sudo e un firewall attivo. Per una guida su come impostare questo, scegliete la vostra distribuzione da questa lista e seguite la nostra guida di configurazione iniziale del server. Assicuratevi di lavorare con una versione supportata di Ubuntu.

  • Conoscenza della riga di comando di Linux. Puoi visitare questa guida su primer sulla riga di comando di Linux.

  • Un’introduzione base alla programmazione in Python e al framework di test in Python pytest. Puoi fare riferimento alla nostra tutorial su Framework di test Python Pytest per ulteriori informazioni su pytest.

  • Python 3.7 o successivo installato sul tuo sistema Ubuntu. Per imparare come eseguire un script Python su Ubuntu, puoi fare riferimento alla nostra guida su Come eseguire un script Python su Ubuntu.

Perché pytest è un Alternativa migliore a unittest

pytest offre diversi vantaggi rispetto al framework integrato unittest:

  • Pytest ti consente di scrivere test con meno codice di configurazione, utilizzando semplici dichiarazioni assert invece delle più lunghe metodi richiesti da unittest.

  • Fornisce un output più dettagliato e leggibile, rendendolo più facile identificare dove e perché un test è fallito.

  • Le fixture di Pytest permettono un setup di test più flessibile e riutilizzabile rispetto ai metodi setUp e tearDown di unittest.

  • Permette di eseguire la stessa funzione di test con multipli insiemi di input, cosa non così semplice in unittest.

  • Pytest ha una ricca collezione di plugin che estendono le sue funzionalità, dai tool di copertura del codice alle esecuzioni di test parallele.

  • Automaticamente scopre file di test e funzioni che corrispondono alle sue convenzioni di nomenclatura, risparmiando tempo e fatica nell’amministrazione degli insiemi di test.

Data questi benefici, pytest è spesso la scelta preferita per i test in Python moderno. Configuriamo una applicazione Flask e scriviamo test unitari usando pytest.

Step 1 – Configurazione dell’Ambiente

Ubuntu 24.04 include Python 3 come predefinito. Apri il terminale e esegui il seguente comando per controllare l’installazione di Python 3:

root@ubuntu:~# python3 --version
Python 3.12.3

Se Python 3 è già installato sulla tua macchina, il comando precedente restituirà la versione corrente dell’installazione di Python 3. In caso contrario, puoi eseguire il seguente comando per installare Python 3:

root@ubuntu:~# sudo apt install python3

Successivamente, devi installare l’installatore pacchetti pip sul tuo sistema:

root@ubuntu:~# sudo apt install python3-pip

Una volta installato pip, installiamo Flask.

Step 2 – Crea una Applicazione Flask

Creiamo prima una semplice applicazione Flask. Crea un nuovo directory per il tuo progetto e naviga in esso:

root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app

Adesso, creiamo e attiviamo un ambiente virtuale per gestire le dipendenze:

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate

Installiamo Flask usando pip:

root@ubuntu:~# pip install Flask

Ora, creiamo una semplice applicazione Flask. Crea un nuovo file chiamato app.py e aggiungi il seguente codice:

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)

Questa applicazione ha tre rotte:

  • /: Restituisce un semplice “Hello, Flask!” messaggio.
  • /about: Restituisce un semplice “This is the About page” messaggio.
  • /multiply/<int:x>/<int:y>: Moltiplica due interi e restituisce il risultato.

Per eseguire l’applicazione, esegui il comando seguente:

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Dall’output precedente noti che il server è in esecuzione su http://127.0.0.1 e sta ascoltando sulla porta 5000. Apri un’altra console Ubuntu e esegui i comandi curl seguenti uno dopo l’altro:

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

Comprendiamo cosa fanno queste richieste GET:

  1. curl http://127.0.0.1:5000/:
    Questa invia una richiesta GET alla rotta radice (‘/’) della nostra applicazione Flask. Il server risponde con un oggetto JSON contenente il messaggio “Hello, Flask!”, dimostrando le funzionalità di base della nostra rotta home.

  2. curl http://127.0.0.1:5000/about:
    Questa invia una richiesta GET alla rotta /about. Il server risponde con un oggetto JSON contenente il messaggio “This is the About page”. Questo dimostra che la nostra rotta sta funzionando correttamente.

  3. curl http://127.0.0.1:5000/multiply/10/20:
    Questa invia una richiesta GET alla rotta /multiply con due parametri: 10 e 20. Il server moltiplica questi numeri e risponde con un oggetto JSON contenente il risultato (200). Questo dimostra che la nostra rotta di moltiplicazione può correttamente processare i parametri URL e eseguire calcoli.

Queste richieste GET ci permettono di interagire con le API endpoint dell’applicazione Flask, recuperando informazioni o attivando azioni sul server senza modificare i dati. Sono utili per recuperare dati, testare la funzionalità degli endpoint e verificare che le nostre rotte rispondono come attese.

Ora vediamo in azione ognuna di queste richieste GET:

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

Step 3 – Installing pytest and Writing Your First Test

Ora che hai una applicazione Flask di base, installa pytest e scrivi qualche test unitario.

Installa pytest utilizzando pip:

root@ubuntu:~# pip install pytest

Crea una directory tests per conservare i tuoi file di test:

root@ubuntu:~# mkdir tests

Adesso, creiamo un nuovo file chiamato test_app.py e aggiungiamo il seguente codice:

test_app.py
# Importa il modulo sys per modificare l'ambiente di runtime di Python
import sys
# Importa il modulo os per interagire con il sistema operativo
import os

# Aggiungi la directory superiore a sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Importa l'istanza dell'app di Flask dal file principale app
from app import app 
# Importa pytest per scrivere e eseguire test
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Ora, analizziamo le funzioni in questo file di test:

  1. @pytest.fixture def client():
    Questa è una fixture di pytest che crea un client di test per la nostra app Flask. Utilizza il metodo app.test_client() per creare un client che può inviare richieste alla nostra app senza avviare il server reale. L’istruzione yield consente al client di essere utilizzato nei test e poi chiuso correttamente dopo ogni test.

  2. def test_home(client):

    Questa funzione testa la route home (/) dell’applicazione nostra. Invia una richiesta GET alla route utilizzando il client di test, quindi asserisce che il codice di stato della risposta sia 200 (OK) e che la risposta JSON corrisponda al messaggio atteso.

  3. def test_about(client):

    Come test_home, questa funzione testa la route about (/about). Controlla un codice di stato 200 e verifica il contenuto della risposta JSON.

  4. def test_multiply(client):

    Questa funzione testa la route di moltiplicazione con un input valido (/multiply/3/4). Controlla che il codice di stato sia 200 e che la risposta JSON contenga il risultato corretto della moltiplicazione.

  5. def test_multiply_invalid_input(client):
    Questa funzione testa la rotta di moltiplicazione con un input non valido (multiply/three/four). Controlla che il codice di stato sia 404 (Non Trovato), che è il comportamento atteso quando la rotta non riesce a corrispondere agli input stringhe ai parametri interi richiesti.

  6. def test_non_existent_route(client):
    Questa funzione testa il comportamento dell’applicazione quando viene accessa una rotta non esistente. Manda una richiesta GET a /non-esistente, che non è definita nel nostro applicazione Flask. Il test dimostra che il codice di stato della risposta è 404 (Non Trovato), garantendo che la nostra applicazione gestisce correttamente le richieste a route non definite.

Questi test coprono la funzionalità di base della nostra applicazione Flask, garantendo che ogni rotta risponda correttamente ai valori validi di input e che la rotta di moltiplicazione gestisca gli input non validi in maniera appropriata. Utilizzando pytest, possiamo facilmente eseguire questi test per verificare che la nostra applicazione funziona come atteso.

Step 4 – Esecuzione delle Prove

Per eseguire le prove, eseguire il seguente comando:

root@ubuntu:~# pytest

Per default, il processo di scoperta di pytest scannerà ricorsivamente la directory corrente e le sue sottodirectory in cerca di file che iniziano con il nome “test_” o che finiscono con “_test”. Le prove contenute in quei file verranno quindi eseguite. Dovresti vedere un output simile a:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

Questo indica che tutte le prove sono state superate con successo.

Step 5: Utilizzo di Fixtures in pytest

Le fixtures sono funzioni che vengono usate per fornire dati o risorse alle prove. possono essere usate per impostare e smettere di utilizzare ambienti di test, caricare dati o eseguire altri compiti di impostazione. In pytest, le fixture vengono definite usando il decoratore @pytest.fixture.

Ecco come migliorare la fixture esistente. Aggiornare la fixture cliente per usare logica di impostazione e smantellamento:

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # In questo punto avviene l'esecuzione delle prove
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

Questa configurazione aggiunge声明 a mostrare le fasi di setup e teardown nell’output dei test. Questi possono essere sostituiti con il codice di gestione delle risorse reali se necessario.

Prova a eseguire i test di nuovo:

root@ubuntu:~# pytest -vs

La flag -v aumenta la verbosità, e la flag -s consente alle声明 di essere visualizzate nell’output del console.

Dovresti vedere l’output seguente:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

Step 6: Adding a Failure Test Case

Aggiungi un caso di test fallito al file di test esistente. Modifica il file test_app.py e aggiungi la funzione seguente verso la fine per un caso di test fallito per un risultato sbagliato:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
    # Test with zero
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

    # Test with large numbers (this might fail if not handled properly)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # Intentional failing test: incorrect result
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

Dividi la funzione test_multiply_edge_cases e spiegami cosa fa ogni parte:

  1. Test with zero:
    Questo test controlla se la funzione di moltiplicazione gestisce correttamente la moltiplicazione per zero. Ci si aspetta che il risultato sia 0 quando si moltiplica qualsiasi numero per zero. Questo è un caso di bordo importante da testare perché alcune implementazioni potrebbero avere problemi con la moltiplicazione per zero.

  2. Test con numeri grandi:
    Questo test verifica se la funzione di moltiplicazione può gestire numeri grandi senza overflow o problemi di precisione. Stiamo moltiplicando due valori da un milione, sperando un risultato di un trilione. Questo test è cruciale perché controlla i limiti superiori della capacità della funzione. Notare che questo potrebbe fallire se l’implementazione del server non gestisce i numeri grandi correttamente, che potrebbe indicare la necessità di librerie per grandi numeri o un diverso tipo di dato.

  3. Test volontariamente fallito:
    Questo test è deliberatamente configurato per fallire. controlla se 2 * 3 equivale a 7, che è sbagliato. Questo test ha lo scopo di dimostrare come appare un test fallito nell’output dei test. Questo aiuta a capire come identificare e risolvere i test falliti, che è una competenza essenziale nell’sviluppo guidato da test e nei processi di debugging.

Includendo questi casi estremi e un fallimento intenzionale, si testa non solo la funzionalità di base della route di moltiplicazione, ma anche il suo comportamento in condizioni estreme e le sue capacità di segnalazione degli errori. Questo approcio alla testatura aiuta a garantire la robustezza e la affidabilità dell’applicazione.

Prova a eseguire i test di nuovo:

root@ubuntu:~# pytest -vs

Dovresti vedere l’output seguente:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # Test con zero response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # Test con numeri grandi (questo potrebbe fallire se non gestito correttamente) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # Test dimostrativo di fallimento: risultato sbagliato response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

Il messaggio di fallimento sopra indica che il test test_multiply_edge_cases del file tests/test_app.py è fallito. In particolare, l’ultima asserzione in questa funzione di test è stata la causa del fallimento.

Questo fallimento intenzionale è utile per dimostrare come vengono riportati i fallimenti dei test e quali informazioni vengono fornite nel messaggio di fallimento. mostra esattamente la riga in cui si è verificato il fallimento, i valori attesi e quelli reali, e la differenza tra i due.

In un scenario reale, si risolverebbe il codice per far passare il test oppure si regolerebbe il test se il risultato atteso era sbagliato. Tuttavia, in questo caso, la fallita è intenzionale a scopo didattico.

Conclusione

In questo tutorial, abbiamo visto come impostare i test unitari per un’applicazione Flask utilizzando pytest, integrare i fixture di pytest e dimostrare come appare una fallita di test. Seguendo questi passaggi, puoi garantire che le tue applicazioni Flask siano affidabili e maintainable, minimizzando i bug e migliorando la qualità del codice.

Puoi fare riferimento alle documentazioni ufficiali di Flask e Pytest per ulteriori informazioni.

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask