Tests unitaires

Qu'est-ce qu'un test unitaire ?

Les tests unitaires sont des tests automatisés permettant d'assurer le bon fonctionnent de parties spécifiques d'un programme (des fonctions) de manière isolée.

Les tests unitaires peuvent être exécutés automatiquement chaque fois que des modifications sont apportées au code. Cela garantit une validation rapide après chaque modification.

En résumé, un test unitaire est une fonction qui teste une autre fonction. À la place de faire des tests manuellement, on les automatise à l'aide de fonctions à exécuter.

Qu'est-ce qu'on veut vérifier dans un test unitaire?

Dans ce cours, le minimum à vérifier serait les points suivants :

  • Vérifier la valeur et le type de retour d’une fonction.

  • Vérifier le contenu d’une liste (valeur, type, nombre d'éléments).

  • Vérifier les cas limites (ex. dépassement de capacité, division par zéro, etc.).

D'autres vérifications existent pour assurer le fonctionnement parfait d'une fonction. Vous aurez l'occasion de les découvrir au fil des exercices (si l'occasion se présente) et dans vos prochains cours de programmation.

Structure d'une fonction de test unitaire : AAA

La méthodologie utilisée pour structurer un test unitaire est AAA pour "Arrange", "Act", et "Assert". Elle est souvent utilisée pour structurer les tests unitaires.

  1. Arrange (Préparer) :

    Mettre en place toutes les conditions préalables nécessaires pour exécuter le test.

    • Préparation et l'initialisation des données.

    • Configuration de l'environnement.

    • Instanciation d'objets.

    • etc.

  2. Act (Agir) :

    Exécuter l'action ou le comportement que vous souhaitez tester.

    • Appel de la fonction à tester,

    • manipulation d'objets,

    • ou toute autre opération qu'on veut évaluer.

  3. Assert (Vérifier) :

    Vérifier que le résultat de l'action effectuée dans la phase "Act" est conforme aux attentes.

    • Si le résultat est celui attendu, le test passe ; sinon, le test échoue.

Méthode de test

Les tests unitaires se font à l'aide de modules intégrés ou tiers. Le module que nous allons utiliser est pytest.

Tests unitaires avec pytest

Voir les étapes décrites ici pour installer le module pytest.

  • Un fichier de test est un fichier python (.py) dont le nom est préfixé par test_ suivi du nom du fichier python à tester. Exemple : test_fonctions_a_tester.py est le fichier dans lequel se trouve les tests unitaires du programme python fonctions_a_tester.py.

  • Le nom de la fonction de test unitaire doit être préfixée par test_. Par soucis de clarté, on fait suivre le préfixe test_ par le nom de la fonction à tester (ou un nom similaire indiquant le test précis).

  • Afin d'utiliser certaines fonctionnalités (décorateurs) de pytest, on doit importer pytest: import pytest.

Les exemples qui vont suivre sont basés sur la structure suivante contenant le code pour les tests unitaires :

fonction_a_tester.py est le fichier du programme dans lequel se trouve les fonctions à tester.

test_fonction_a_tester.py est le fichier dans lequel se trouve les fonction de test du fichier fonction_a_tester.py.

Syntaxe des vérifications assert

assert condition[, "message d'erreur optionnel"]
  • condition : la condition qui exprime le résultat attendu est évaluée. Si le résultat est vrai, le test passe ; sinon, le test échoue.

  • Le message d'erreur est optionnel.

Exemples

assert resultat == resultat_attendu
assert resultat in liste_resultats_attendus
assert min_resultat_attendu < resultat  < max_resultat_attendu 
...

Exemple d'un test unitaire

Fonction à tester :

fonction_a_tester.py
def division_entiere_1(n, m):
    return n//m

Exemple d'un test unitaire de base pour la fonction précédente :

Voir la structure AAA pour plus de détails sur # Arrange # Act et # Assert.

test_fonction_a_tester.py
import pytest
import Tests_unitaires.projet_pour_tests_unitaires.fonctions_a_tester as fonctions_a_tester

def test_division_entiere_1():
    # Arrange
    n = 6
    m = 2
    resultat_attendu = 3
    # Act
    resultat_division = fonctions_a_tester.division_entiere_1(n, m)

    # Assert
    assert resultat_division == resultat_attendu
  • Arrange : on définit les données de test n=6 et m=2.

  • Act : on appelle la fonction division_entiere_1 (qu'on veut tester).

  • Assert : on vérifie si le résultat de la division de n=6 sur m=2 est bien égal à 3.

Les données de test

Avant d'écrire un test unitaire, on doit préparer un plan de tests contenant les données à tester. Ce données sont mentionnées au dessus d'un test unitaire :

Syntaxe :

@pytest.mark.parametrize("p1, p2, ..., resultat_attendu", [
    (d1_p1, d1_p2, ..., resultat_attendu1),
    (d2_p1, d2_p2, ..., resultat_attendu2),
    ...,
    (dN_p1, dN_p2, ..., resultat_attenduN),
])
def test_fonction(p1, p2, ..., resultat_attendu):
    # Instructions qui utilisent les données des 
    # paramètres dans les sections Arrange/Act/Assert

Exemple :

# Arrange
@pytest.mark.parametrize("n, m, resultat_attendu", [
    (10, 2, 5),
    (-10, 2, -5),
    (3, 2, 1),
    (1000000, 10, 100000),
    (0, 5, 0),
    (1, 3, 0)
])
def test_division_entiere_1(n, m, resultat_attendu):
    # Arrange --> c'est le décorateur @pytest.mark.parametrize au dessus de la définition de la fonction qui prépare les données de tests.

    # Act
    resultat = fonctions_a_tester.division_entiere_1(n, m)

    # Assert
    assert isinstance(resultat, int)
    assert resultat == resultat_attendu

Vérifications (Assert) minimales à faire (selon le type du résultat retourné)

Vérifications pour les types simples

La vérification
Syntaxe
Exemple

Vérifier que le résultat retourné par la fonction a tester a le bon type.

assert isinstance(<resultat>, <type>)
assert isinstance(resultat, int)

Vérifier que le résultat est celui attendu.

assert <condition>
assert resultat == resultat_attendu
assert resultat in liste_resultats_attendus
assert min_resultat_attendu < resultat  < max_resultat_attendu 

Vérifications pour les résultats de type liste

La vérification
Exemple

Vérifier que le résultat est bien de type list

assert isinstance(<resultat>, list)

Vérifier que la longueur de la liste est celle attendue

Pour vérifier l'ajout d'un élément dans une liste.

  • La variable longueur_init est la longueur initiale de la liste avant l'ajout de l'élément.

  • La variable resultat contient la liste retournée par l'appel à la fonction.

longueur_resultat = len(resultat)
assert longueur_resultat  == longueur_init + 1

Vérifier que chaque élément de la liste a le bon type.

for e in resultat:
    assert isinstance(e, int)
for e in resultat:
    assert isinstance(e, int) or isinstance(e, float)
assert all(isinstance(e, float) for e in resultat)

À l'ajout d'un élément à une liste : vérifier que l'élément ajouté est bien à la bonne place.

assert element_ajoute in resultat
assert resultat[position] == element_ajoute

À la suppression d'un élément dans une liste : L'élément supprimé n'existe plus dans la liste (not in).

assert element_supprime not in resultat

À la modification d'un élément dans une liste : - la modification de l'élément a bien été faite correctement. - L'ancien élément avant la modification n'existe plus dans la liste.

assert resultat[position] == element_modifie
assert element_avant_modif not in resultat

Vérifications pour les exceptions

La vérification
Syntaxe
Exemple

Vérifier qu'une exception est gérée

@pytest.mark.xfail(raises=<type_exception>)
def test_fonction():

    fonctions_a_tester(...)
@pytest.mark.xfail(raises=TypeError)
def test_fonction():

    fonctions_a_tester(...)

Exemples

fonction_a_tester.py
def division_entiere(n, m):
    try:
        return n//m
    except ZeroDivisionError:
        return None

@pytest.mark.parametrize("a, b, resultat_attendu", [
    (10, 2, 5),
    (-10, 2, -5),
    (3, 2, 1),
    (1000000, 10, 100000),
    (0, 5, 0),
    (1, 3, 0)
])
def test_division_entiere(a, b, resultat_attendu):
    # Arrange
    # Act
    resultat = demo.division_entiere(a, b)

    # Assert
    assert isinstance(resultat, int)
    assert resultat == resultat_attendu

# Ici, l'exception ZeroDivisionError est gérée, ce teste va donc passer.
@pytest.mark.xfail(raises=ZeroDivisionError)
def test_exception_zero_division_entiere():
    resultat = demo.division_entiere(5, 0)
    resultat_attendu = 999999999
    assert resultat == resultat_attendu
    
# Ici, l'exception TypeError n'est pas gérée, ce teste NE va donc PAS passer.
@pytest.mark.xfail(raises=TypeError)
def test_exception_TypeError_division_entiere():
    resultat = demo.division_entiere("10", "5")
    resultat_attendu = None
    assert resultat == resultat_attendu

Exercices guidés

Pour chaque fonction :

  • Faites le plan de test : les données d'entrée, le résultat attendu

  • Listez les vérifications (assert) à faire.

  • Créez votre teste unitaire sur la base des deux points précédents.

Fonction 1 : generer_adresses_courriel

def generer_adresses_courriel(prenom:str, nom:str, domaine:str)->str:
    """
    Fonction qui génère une adresse courriel au format
    [nom].[prenom]@[domaine] à partir d'un prenom, nom et domaine.
    :param prenom: Le prenom de la personne.
    :param nom: Le nom de la personne.
    :param domaine: Le nom de domaine pour l'adresse courriel
    :return: L'adresse courriel au format [nom].[prenom]@[domaine]
             tout en lettres minuscules.
    """

    adresse = f"{prenom.lower()}.{nom.lower()}@{domaine}"

    return adresse

Test unitaire de la fonction generer_adresses_courriel

Test unitaire de la fonction generer_adresses_courriel
# Arrange
@pytest.mark.parametrize("prenom, nom, domaine, resultat_attendu", [
    ("Alice", "Smith", "company.org", "alice.smith@company.org"),
    ("bob", "Johnson", "webmail.net", "bob.johnson@webmail.net"),
    ("Eva", "lee", "personal.info", "eva.lee@personal.info")
    # Ajoutez d'autres cas de test si nécessaire
])
def test_generer_adresses_courriel(prenom, nom, domaine, resultat_attendu):
    # Act
    resultat = fonctions_a_tester.generer_adresses_courriel(prenom, nom, domaine)

    # Assert
    assert isinstance(resultat, str)
    assert resultat == resultat_attendu

Fonction 2 : ajouter_tache

[...cet exemple n'est pas très pertinent car la méthode .insert(...) sur les listes est écrite correctement et fait déjà des vérifications mais l'idée est de réfléchir à ce qu'il faudrait vérifier dans un test unitaire...]

def ajouter_tache(liste_taches, tache, priorite):
    """
    Ajoute une tâche à une liste de tâches selon sa priorité dans la liste.
    :param liste_taches: la liste des tâches existante.
    :param tache: la tâche à ajouter dans la liste.
    :param priorite: La position à laquelle on ajoute la tâche.
    :return: retourne la liste incluant la tâche ajoutée.
    """
    liste_taches.insert(priorite, tache)
    return liste_taches

Tests unitaires de la fonction ajouter_tache

Test unitaire de la fonction ajouter_tache
# Arrange
@pytest.mark.parametrize("liste_taches, tache, priorite, resultat_attendu", [
    (["Faire les courses"], "Répondre aux courriels", 0, ["Répondre aux courriels", "Faire les courses"]),
    (["Travailler sur le projet", "Préparer le dîner", "Lire un livre"], "Faire du sport", 2, ["Travailler sur le projet", "Préparer le dîner", "Faire du sport", "Lire un livre"]),
    ([], "Commencer le rapport", 0, ["Commencer le rapport"]),
    (["Réviser pour l'examen"], "Prendre une pause", 1, ["Réviser pour l'examen", "Prendre une pause"]),
    # Ajoutez d'autres cas de test si nécessaire
])
def test_ajouter_tache(liste_taches, tache, priorite, resultat_attendu):

    # Arrange
    longueur_init = len(liste_taches)

    # Act
    resultat = fonctions_a_tester.ajouter_tache(liste_taches, tache, priorite)

    # Assert
    assert len(liste_taches) == longueur_init + 1
    assert isinstance(resultat, list)
    assert all(isinstance(tache, str) for tache in resultat)
    assert resultat == resultat_attendu
    assert resultat[priorite] == tache

Last updated