Comment effectuer des tests unitaires dans Flask

Introduction

Le test est essentiel au processus de développement du logiciel, il garantit que le code fonctionne comme prévu et est exempt de défauts. En Python, pytest est un framework de test populaire offrant plusieurs avantages sur le module unit test standard, qui est un framework de test intégré à Python et fait partie de la bibliothèque standard. pytest propose une syntaxe plus simple, une sortie améliorée, des fixtures puissants et une écosystème de plugins riche. Ce tutoriel vous guidera dans la configuration d’une application Flask, l’intégration de fixtures pytest et l’écriture de tests unitaires à l’aide de pytest.

Prérequis

Avant de commencer, vous serez nécessité de disposer des éléments suivants :

  • Un serveur fonctionnant sous Ubuntu et un utilisateur non-root avec des privilèges sudo et une firewall active. Pour des instructions sur la mise en place, veuillez sélectionner votre distribution à partir de cette liste et suivez notre guide de configuration initiale du serveur. Veuillez vous assurer de travailler avec une version supportée d’Ubuntu.

  • Familiarité avec la ligne de commande Linux. Vous pouvez consulter ce guide sur préliminaires de la ligne de commande Linux.

  • Un petit savoir-faire en programmation Python et dans le framework de test pytest. Vous pouvez vous référer à notre tutoriel sur le Pytest Testing Framework pour en apprendre davantage sur pytest.

  • Python 3.7 ou plus élevé installé sur votre système Ubuntu. Pour apprendre comment exécuter un script Python sur Ubuntu, vous pouvez vous référer à notre tutoriel sur How to run a Python script on Ubuntu.

Why pytest is a Better Alternative to unittest

pytest offre plusieurs avantages sur le framework intégré unittest :

  • Pytest vous permet d’écrire des tests avec moins de code de base, en utilisant des assertions simples au lieu des méthodes plus verbeuses requises par unittest.

  • Il offre des sorties plus détaillées et lisibles, ce qui facilite l’identification des points de défaillance des tests.

  • Les fixations de tests de Pytest permettent des configurations de tests plus flexibles et réutilisables que les méthodes setUp et tearDown d’unittest.

  • Il permet de facilement exécuter la même fonction de test avec plusieurs jeux d’entrées, ce qui n’est pas aussi aisé avec unittest.

  • Pytest dispose d’une riche collection de plugins qui étendent ses fonctionnalités, allant des outils de couverture de code à l’exécution des tests parallèles.

  • Il détecte automatiquement les fichiers de tests et les fonctions correspondant à ses conventions de nommage, économisant temps et efforts dans la gestion des suites de tests.

Compte tenu de ces avantages, pytest est souvent le choix préféré pour les tests modernes en Python. Allons configurer une application Flask et écrire des tests unitaires en utilisant pytest.

Étape 1 – Configuration de l’environnement

Ubuntu 24.04 embarque Python 3 par défaut. Ouvrez le terminal et exécutez la commande suivante pour vérifier la présence de Python 3 :

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

Si Python 3 est déjà installé sur votre machine, la commande ci-dessus retournera la version actuelle de l’installation de Python 3. Si il n’est pas installé, vous pouvez exécuter la commande suivante pour l’installer :

root@ubuntu:~# sudo apt install python3

Ensuite, vous devez installer l’outil d’installation de packages pip sur votre système :

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

Une fois pip installé, installez Flask.

Étape 2 – Créer une application Flask

Commencez par créer une application Flask simple. Créez un nouveau répertoire pour votre projet et naviguez-y :

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

Maintenant, créez et activez un environnement virtuel pour gérer les dépendances :

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

Installez Flask en utilisant pip :

root@ubuntu:~# pip install Flask

Maintenant, créez une simple application Flask. Créez un nouveau fichier nommé app.py et ajoutez le code suivant :

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)

Cette application a trois routes :

  • / : Retourne un simple message “Bonjour, Flask!”.
  • /about : Retourne un simple message “Ceci est la page A propos”.
  • /multiply/<int:x>/<int:y> : Multiplie deux entiers et retourne le résultat.

Pour exécuter l’application, exécutez la commande suivante :

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)

A partir de l’affichage ci-dessus, vous pouvez remarquer que le serveur fonctionne sur http://127.0.0.1 et écoute sur le port 5000. Ouvrez une autre console Ubuntu et exécutez les commandes curl suivantes une par une :

  • 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

L’action effectuée par ces requêtes GET est la suivante :

  1. curl http://127.0.0.1:5000/ :
    Cette commande envoie une requête GET à l’URL racine (‘/’) de notre application Flask. Le serveur répond avec un objet JSON contenant le message “Hello, Flask!”, montrant ainsi la fonctionnalité de base de notre route d’accueil.

  2. curl http://127.0.0.1:5000/about :
    Cette commande envoie une requête GET à la route /about. Le serveur répond avec un objet JSON contenant le message “This is the About page”. Cela montre que notre route fonctionne correctement.

  3. curl http://127.0.0.1:5000/multiply/10/20 :
    Cette commande envoie une requête GET à la route /multiply avec deux paramètres : 10 et 20. Le serveur multiplie ces nombres et répond avec un objet JSON contenant le résultat (200). Cela montre que notre route de multiplication peut correctement traiter les paramètres d’URL et effectuer des calculs.

Ces requêtes GET permettent de communiquer avec les points d’accès de l’API de notre application Flask, sans modifier les données. Elles sont utiles pour récupérer des données, tester la fonctionnalité des points d’accès, et vérifier que nos routes répondent comme prévu.

Affichez maintenant chacune de ces requêtes GET en action :

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}

Étape 3 – Installation de pytest et écriture de votre premier test

Maintenant que vous avez une application Flask de base, installez pytest et écrivez quelques tests unitaires.

Installez pytest en utilisant pip :

root@ubuntu:~# pip install pytest

Créez un dossier tests pour stocker vos fichiers de tests :

root@ubuntu:~# mkdir tests

Maintenant, créez un nouveau fichier nommé test_app.py et ajoutez le code suivant :

test_app.py
# Import sys module for modifying Python's runtime environment
import sys
# Import os module for interacting with the operating system
import os

# Add the parent directory to sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Import the Flask app instance from the main app file
from app import app 
# Import pytest for writing and running tests
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

Division du fichier de test :

  1. @pytest.fixture def client():
    Ceci est une fonction fixture de pytest qui crée un client de test pour notre application Flask. Elle utilise la méthode app.test_client() pour créer un client capable d’envoyer des demandes à notre application sans exécuter le serveur réel. L’instruction yield permet à ce client d’être utilisé dans les tests puis d’être fermé correctement après chaque test.

  2. def test_home(client):
    Cette fonction teste la route acceuil (/) de notre application. Elle envoie une requête GET à la route en utilisant le client de test, puis elle vérifie que le code de statut de la réponse est 200 (OK) et que la réponse JSON correspond au message attendu.

  3. def test_about(client):
    Comme test_home, cette fonction teste la route à propos (/about). Elle vérifie un code de statut de 200 et vérifie le contenu de la réponse JSON.

  4. def test_multiply(client):
    Cette fonction teste la route de multiplication avec une entrée valide (/multiply/3/4). Elle vérifie que le code de statut est 200 et que la réponse JSON contient le résultat correct de la multiplication.

  5. def test_multiply_invalid_input(client):
    Cette fonction teste la route de multiplication avec une entrée invalide (multiply/three/four). Elle vérifie que le code statut est 404 (Pas trouvé), ce qui est le comportement attendu lorsque la route ne peut pas correspondre les entrées de chaîne aux paramètres entiers requis.

  6. def test_non_existent_route(client):
    Cette fonction teste le comportement de l’application lorsqu’une route inexistante est accédée. Elle envoie une requête GET vers /non-existent, qui n’est pas définie dans notre application Flask. Le test vérifie que le code statut de la réponse est 404 (Pas trouvé), garantissant que notre application traite correctement les requêtes vers des routes non définies.

Ces tests couvrent les fonctionnalités de base de notre application Flask, garantissant que chaque route répond correctement aux entrées valides et que la route de multiplication traite les entrées invalides appropriément. En utilisant pytest, nous pouvons facilement exécuter ces tests pour vérifier que notre application fonctionne comme prévu.

Step 4 – Exécution des tests

Pour exécuter les tests, exécutez la commande suivante :

root@ubuntu:~# pytest

Par défaut, le processus de découverte de pytest recherche récursivement les fichiers commençant par « test_ » ou se terminant par « _test » dans le dossier courant et ses sous-dossiers. Les tests situés dans ces fichiers sont ensuite exécutés. Vous devriez voir un affichage similaire à :

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 ========================================================

Cela indique que tout les tests ont été passés avec succès.

Étape 5 : Utiliser des fixations dans pytest

Les fixations sont des fonctions utilisées pour fournir des données ou des ressources aux tests. Elles peuvent être utilisées pour configurer et déconfigurer des environnements de test, charger des données ou effectuer d’autres tâches de configuration. Dans pytest, les fixations sont définies en utilisant le décorateur @pytest.fixture.

Voici comment améliorer la fixation existante. Mettez à jour la fixation du client pour utiliser de la logique de configuration et de déconfiguration :

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  # C'est ici que les tests se déroulent
    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

Cet ensemble de configurations ajoute des instructions d’affichage pour démontrer les phases d’installation et de démontage dans les sorties de tests. Ces instructions peuvent être remplacées par du code de gestion des ressources réelles si nécessaire.

essayons de lancer les tests à nouveau :

root@ubuntu:~# pytest -vs

Le flag -v augmente la verbosité, et le flag -s permet l’affichage des instructions d’affichage dans la sortie de la console.

Vous devriez voir l’affichage suivant :

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 =============================================

Étape 6 : Ajout d’un cas de test d’échec

Ajoutons un cas de test d’échec au fichier de tests existant. Modifiez le fichier test_app.py et ajoutez la fonction ci-après à la fin pour un cas de test d’échec de résultat incorrect :

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

    # Test avec de grands nombres (cela pourrait échouer si il n'est pas géré correctement)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # Test有意失败:结果错误
    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"

Divisons la fonction test_multiply_edge_cases et expliquons ce que chaque partie fait :

  1. Test avec zéro :
    Ce test vérifie si la fonction de multiplication gère correctement la multiplication par zéro. Nous attendons un résultat de 0 lorsque nous multiplions n’importe quel nombre par zéro. C’est un cas extrême important à tester parce que certaines implémentations pourraient avoir des problèmes avec la multiplication par zéro.

  2. Test avec des nombres importants :
    Ce test vérifie si la fonction de multiplication peut gérer des nombres importants sans débordement ou sans problème de précision. Nous multiplions deux valeurs de un million, attendant un résultat de un billion. Ce test est crucial car il vérifie les limites supérieures des capacités de la fonction. Notez que cela pourrait échouer si l’implémentation du serveur ne gère pas correctement les grands nombres, ce qui pourrait indiquer la nécessité d’utiliser des bibliothèques pour les nombres grands ou un type de données différent.

  3. Test prévu pour échouer :
    Ce test est délibérément configuré pour échouer. Il vérifie si 2 * 3 égal 7, ce qui est incorrect. Ce test a pour but de montrer comment un test échouant apparaît dans la sortie de test. Cela permet de mieux comprendre comment identifier et débugger les tests échouants, ce qui est une compétence essentielle dans le développement guidé par les tests et les processus de débogage.

En incluant ces cas limites et un échec intentionnel, vous testez non seulement la fonctionnalité de base de votre route de multiplication, mais aussi son comportement dans des conditions extrêmes et ses capacités de rapport d’erreurs. Cette approche de test aide à assurer la robustesse et la fiabilité de notre application.

Faisons en sorte de relancer les tests à nouveau :

root@ubuntu:~# pytest -vs

Vous devriez voir la sortie suivante :

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 avec zéro response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # Test avec de grands nombres (cela pourrait échouer si cela n'est pas géré correctement) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # Test intentionnellement raté : résultat incorrect 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 ========================================================

Le message d’échec ci-dessus indique que le test test_multiply_edge_cases dans le fichier tests/test_app.py a échoué. Plus précisément, la dernière assertion dans cette fonction de test a causé l’échec.

Cet échec intentionnel est utile pour montrer comment les échecs de test sont rapportés et quels informations sont fournies dans le message d’échec. Il montre la ligne exacte où l’échec s’est produit, les valeurs attendues et actuelles, et la différence entre les deux.

Dans une scénario réel, vous corrigeriez le code pour que le test passe ou ajusteriez le test si le résultat attendu était incorrect. Cependant, dans ce cas, l’échec est intentionnel pour des raisons éducatives.

Conclusion

Dans ce tutoriel, nous avons vu comment configurer des tests unitaires pour une application Flask en utilisant pytest, des fixures pytest intégrées et montré ce que doit ressembler un échec de test. En suivant ces étapes, vous pouvez être sûr que vos applications Flask sont fiables et maintenables, en réduisant le nombre de bugs et en améliorant la qualité du code.

Vous pouvez vous référer à la documentation officielle de Flask et de Pytest pour en savoir plus.

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