Pour quelles raisons tester son code ?
Dans un précédent article, nous avons vu pourquoi les tests unitaires sont fondamentaux pour assurer la qualité et le bon fonctionnement d’un code.
Pour rappel, les tests unitaires permettent de tester isolément différentes parties d’un code contrairement aux tests d’intégration qui vérifient le fonctionnement du code dans son ensemble. Ils se différencient également des tests fonctionnels (functional testing) qui permettent de s’assurer du fonctionnement d’une fonctionnalité spécifique.
Pourquoi utiliser le framework Pytest pour tester son code ?
Afin de faciliter l’automatisation des tests unitaires, il est possible d’utiliser des frameworks de test ou « testing frameworks » qui permettent de normaliser leur écriture, d’identifier rapidement les tests qui échouent et de faciliter leur maintenance .
Sur Python, il existe différents frameworks de tests dont les plus connus sont Unittest et Pytest. Aujourd’hui, nous nous concentrerons sur l’implémentation de tests unitaires avec Pytest dont la prise en main est un peu plus simple.
Comment utiliser Pytest ?
Tout d’abord, il vous faudra installer la librairie Pytest avec la commande pip install pytest.
Dans un dossier, nous créons un fichier student.py. À l’intérieur nous créons une classe Student qui permet de créer des instance d’étudiants, d’enregistrer leurs notes et de calculer leur moyenne.
class Student: #creation of a student class
def __init__(self, grades = []):
self.grades = grades
self.count_grades = len(grades)
if len(grades) == 0 :
self.academic_average = 0
else :
self.academic_average = sum(grades) / len(grades)
def add_grade(self, grade) :
if (grade < 0) | (grade > 20) :
raise InvalidGrade("Grade should be between 0 and 20")
else :
self.grades.append(grade)
class InvalidGrade(Exception):
pass
Dans le même dossier, nous créons un fichier de test student_test.py. Le nom des fichiers de test doivent s’écrire sous le format « test_nom_du_fichier.py » ou « nom_du_fichier_test.py ». Dans le fichier, chaque test devra être défini par une fonction préfixée par « test ».
from student import Student
import pytest # importing our library of test
# 3 test cases are defined in this test suite
def test_create_student_grades():
student = Student()
assert(student.grades) == []
def test_create_student_average():
assert student.academic_average == 0
def test_add_grades():
student = Student()
student.add_grade(12)
assert student.academic_average == 12
Les assert statements permettent de vérifier que la fonction renvoie un résultat correct pour des arguments donnés et de lever une exception si la condition est fausse. Dans notre exemple, les deux premières fonctions vérifient les propriétés (grades, academic average) d’un objet de type Student lors de son instanciation puis la valeur de la moyenne après ajout d’une note de 12.
Si les fichiers de tests sont correctement préfixés, il suffira de lancer la commande python -m pytest dans votre terminal pour les lancer. Pour lancer un test en particulier, exécutez la commande python -m pytest nom_du_fichier.py.
Après exécution, Pytest fournit un rapport détaillé des tests.
========================================================= test session starts==============================================================
collected 3 items
student_test.py ... [100%]
========================================================= 3 passed in 0.02s =========================================================
Les « collected items » correspondent au nombre de tests qui sont exécutés. Ici, nos 3 tests sont passés.
Nous modifions temporairement notre fichier student.py pour qu’à l’instanciation d’un étudiant, il ait par défaut eu une note de 10.
class Student:
def __init__(self, grades = [10]): #the student will now have a default grade of 10 when he is created
self.academic_average = average
self.count_grades = len(grades)
if len(grades) == 0 :
self.academic_average = 0
else :
self.academic_average = sum(grades) / len(grades)
def add_grade(self, grade) :
....
Dans notre fichier student_test.py, nous masquons les 2 premières fonctions de test pour conserver uniquement la fonction de test_add_grades, puis nous exécutons le fichier de test.
from student import Student
import pytest # importing our library of test
def test_add_grades():
student = Student()
student.add_grade(12)
assert student.academic_average == 12
=============================== test session starts =============================
collected 1 items
============================================================ FAILURES===================================================================
student_test.py .. F [100%]
__________________________________________________________test_add_grades________________________________________________________________
def test_add_grades():
student = Student()
student.add_grade(12)
> assert(student.academic_average) == 12
E assert 11.0 == 12
E + where 11.0 = <student.Student object at 0x7f30ba220d00>.academic_average
student_test.py:7: Assertion
===========================)======================== short test summary info ============================================================
FAILED student_test.py::test_add_grades - assert 11.0 == 12
=======================================================1 failed in 0.03s ================================================================
Nous constatons que le test ne passe plus car les modifications effectuées dans notre fichier student.py rendent l’assertion fausse. Pytest nous permet ainsi de voir si les modifications d’un code impactent ou non le résultat attendu.
NB : pour la suite, il sera nécessaire de modifier la fonction init de student.py pour revenir à une liste vide lors de l’instanciation.
class Student:
def __init__(self, grades = []): #reset default grades to an empty list
self.academic_average = average
self.count_grades = len(grades)
if len(grades) == 0 :
self.academic_average = 0
else :
self.academic_average = sum(grades) / len(grades)
def add_grade(self, grade) :
....
Éviter la répétition avec les pytest fixtures
Dans l’exemple précédent, nous avons instancié un objet de type Student dans chaque test. Pour éviter de réécrire un code utilisé dans plusieurs tests, Pytest dispose d’un décorateur spécifique appelé fixture qui permet d’accéder facilement aux éléments nécessaires à l’exécution d’un test, de la donnée par exemple. Pour spécifier qu’une fonction est une fixture, il sera nécessaire d’utiliser le décorateur @pytest.fixture avant de la définir. Il faudra également passer la fixture en argument des fonctions de tests où vous souhaitez l’appeler.
from student import Student
import pytest
@pytest.fixture #test fixture decorator
def student():
return Student() #create an instance of student
def test_create_student_grades():
assert(student.grades) == []
def test_create_student_average():
assert(student.academic_average) == 0
def test_add_grades():
student.add_grade(12)
assert(student.academic_average) == 12
Dans cet exemple, la fixture nous évite de ré-instancier l’objet étudiant dans chaque test.
Le décorateur parametrize tests pour tester une suite d’instructions
from student import Student
import pytest
@pytest.mark.parametrize("grade_1,grade_2, grade_3, average", [(12, 10, 8, 10), (20, 18, 16, 18), (3,9,9,7)])
def test_average(grade_1, grade_2, grade_3, average):
student = Student()
student.grades = [] #Set an empty list of grades each time the test runs
student.add_grade(grade_1)
student.add_grade(grade_2)
student.add_grade(grade_3)
assert student.academic_average == average
La paramétrisation permet d’exécuter le même test avec différentes valeurs. Pour cela nous utilisons le décorateur @pytest.mark.parametrize. Il faudra préciser le nom des arguments à tester ainsi que leur valeur.
Conclusion
Dans cet article, nous nous sommes initiés à l’utilisation de Pytest pour tester un code. En tant que Data Engineer ou Data Scientist, la mise en place de tests unitaires est une étape nécessaire pour garantir la qualité d’un code et réduire les problèmes lors du déploiement. Il faut néanmoins rappeler que cette étape n’est pas suffisante. D’une part, lors du développement d’une application de Machine Learning, certains éléments peuvent passer entre les mailles du filet. De plus, les tests unitaires ne peuvent, par définition, tester l’interaction entre les unités.
Pour découvrir le métier de Data Engineer et le parcours de formation que nous proposons, n’hésitez pas à consulter notre page dédiée.
{
« @context »: « https://schema.org »,
« @type »: « FAQPage »,
« mainEntity »: [{
« @type »: « Question »,
« name »: « Pourquoi utiliser le framework Pytest pour tester son code ? »,
« acceptedAnswer »: {
« @type »: « Answer »,
« text »: « Afin de faciliter l’automatisation des tests unitaires, il est possible d’utiliser des frameworks de test ou « testing frameworks » qui permettent de normaliser leur écriture, d’identifier rapidement les tests qui échouent et de faciliter leur maintenance . »
}
},{
« @type »: « Question »,
« name »: « Comment éviter la répétition avec les pytest fixtures ? »,
« acceptedAnswer »: {
« @type »: « Answer »,
« text »: « Pour éviter de réécrire un code utilisé dans plusieurs tests, Pytest dispose d’un décorateur spécifique appelé fixture qui permet d’accéder facilement aux éléments nécessaires à l’exécution d’un test, de la donnée par exemple. Pour spécifier qu’une fonction est une fixture, il sera nécessaire d’utiliser le décorateur @pytest.fixture avant de la définir. Il faudra également passer la fixture en argument des fonctions de tests où vous souhaitez l’appeler. »
}
}]
}

