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.
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.
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.
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.
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 pythonfonctions_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
.
Le nom de la fonction doit obligatoirement commencer par le mot clé test_.
pour qu'elle soit reconnue comme étant un test unitaire.
Bonne pratique :
On créé un projet de test pour chaque projet python à tester.
Dans un projet de test, on créé un fichier de test par fichier .py du projet à tester.
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
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 :
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.
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
etm=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
surm=2
est bien égal à 3.
Isolation : une unité spécifique de code doit être testée seule pour s'assurer qu'elle fonctionne indépendamment des autres parties du programme.
On teste une seule fonction à la fois (une fonction de test teste une seule fonction). Il peut y avoir plusieurs fonctions de test pour une seule fonction à tester.
On n'appelle pas d'autres fonctions qui ont besoin d'être testées (autre que celle qu'on veut tester).
Précision :
Les tests unitaires doivent couvrir divers scénarios de données et vérifier que l'unité testée produit les résultats attendus dans toutes les situations.
Pour une fonction à tester, on regroupe les tests du même type dans la même fonction de test.
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
assert isinstance(<resultat>, <type>)
assert isinstance(resultat, int)
Vérifications pour les résultats de type liste
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
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
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
# 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
# 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