Les tests unitaires dans Flask¶
Les tests unitaires peuvent s'utiliser dans Flask tels que nous l'avons vu dans la section précédente.
Nous allons reprendre l'exemple du site de Poudlard vu en cours.
Rappel du site de Poudlard¶
Le site de Poudlard a un modèle qui permet de récupérer les étudiants par maison.
1from poudlard.db import get_db
2
3def get_etudiant_by_maison():
4 """
5 Retourne un dictionnare avec comme clé les maisons, et comme valeur la liste
6 des étudiants dans cette maison.
7 """
8 db = get_db()
9 etudiants = db.execute(
10 'SELECT * FROM etudiant ORDER BY maison, nom, prenom')
11 maisons = {}
12 for etudiant in etudiants:
13 if etudiant["maison"] not in maisons:
14 etudiant_maison = []
15 maisons[etudiant["maison"]] = etudiant_maison
16 etudiant_maison.append(etudiant)
17 return maisons
Mais est-ce que cette fonction marche bien ? On va la tester.
Ecriture d'un test¶
Nous allons écrire les tests dans un package « tests » (c'est-à-dire un dossier avec un fichier __init__.py vide) dans le dossier de notre application. Le fichier de test pour les étudiants peut s'appeler par exemple test_etudiant.py
.
Nous créons une classe TestUser qui étend unittest.TestCase
comme pour une application Python quelconque. Nous allons écrire un test pour la fonction get_etudiant_by_maison
.
Nous allons pour l'instant assumer qu'il n'y a que l'étudiant Harry Potter de la maison Gryffondor dans la base de donnée pour l'instant. Nous reviendrons sur ce point plus tard.
Nous allons tester si la fonction retourne bien un dictionnaire avec une seule maison, Gryffondor, et un seul étudiant dont le nom de famille est "Potter".
class TestUser(unittest.TestCase):
def test_search_by_maison(self):
maisons_dict = get_etudiant_by_maison()
self.assertEqual(len(maisons_dict), 1)
maisons = list(maisons_dict.keys())
self.assertEqual(len(maisons), 1)
self.assertEqual(maisons[0], 'Gryffondor')
etudiants = maisons_dict['Gryffondor']
self.assertEqual(etudiants[0]['nom'], 'Potter')
Base de donnée de test¶
Pour tester cette fonction, nous allons créer une base de donnée de test. En effet, il est important de tester les fonctions avec des données de test, et non pas avec les données réelles. Nous risquerions de corrompre la base de donnée de production. Il est de plus très difficile de réfléchir sur une grosse base de donnée pour écrire des tests.
Unittest fournit une méthode setUp
qui est appelée avant chaque test. Nous allons donc créer une base de donnée de test dans cette méthode. Nous allons aussi créer une méthode tearDown
qui sera appelée après chaque test pour nettoyer la base de donnée.
def setUp(self):
# generate a temporary file for the test db
self.db_fd, self.db_path = tempfile.mkstemp()
# create the testapp with the temp file for the test db
self.app = create_app({'TESTING': True, 'DATABASE': self.db_path})
self.app_context = self.app.app_context()
self.app_context.push()
self.db = get_db()
# read in SQL for populating test data
with open(os.path.join(os.path.dirname(__file__), "schema_test.sql"), "rb") as f:
self.db.executescript(f.read().decode("utf8"))
def tearDown(self):
# closing the db and cleaning the temp file
close_db()
os.close(self.db_fd)
os.unlink(self.db_path)
schema_test.sql
contient le code pour remplire la base de donnée de test. Soit vous la créez à la main, de toute pièce, soit vous utilisez la base de donnée réelle et vous la simplifiez.
Pour la créer à partir de la base de données réelle :
$ sqlite3 poudlard/poudlard.sqlite
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .output schema_test.sql
sqlite> .dump
sqlite> .exit
Et puis on simplifie, par exemple en ne gardant que Harry Potter dans les INSERT INTO:
1PRAGMA foreign_keys=OFF;
2BEGIN TRANSACTION;
3CREATE TABLE IF NOT EXISTS "etudiant" (
4 "etudiant_id" INTEGER NOT NULL UNIQUE,
5 "nom" TEXT NOT NULL,
6 "prenom" TEXT NOT NULL,
7 "date_naissance" TEXT,
8 "maison" TEXT,
9 PRIMARY KEY("etudiant_id" AUTOINCREMENT)
10);
11
12INSERT INTO etudiant VALUES(1,'Potter','Harry','1980/07/31','Gryffondor');
13
14CREATE TABLE IF NOT EXISTS "inscription" (
15 "inscription_id" INTEGER NOT NULL UNIQUE,
16 "etudiant_id" INTEGER NOT NULL,
17 "date_inscription" TEXT,
18 "acad_year" INTEGER,
19 PRIMARY KEY("inscription_id" AUTOINCREMENT),
20 FOREIGN KEY("etudiant_id") REFERENCES "etudiant"("etudiant_id")
21);
22INSERT INTO inscription VALUES(4,1,NULL,1991);
23
24CREATE TABLE IF NOT EXISTS "professeur" (
25 "professeur_id" INTEGER NOT NULL UNIQUE,
26 "nom" TEXT NOT NULL,
27 "prenom" TEXT NOT NULL,
28 PRIMARY KEY("professeur_id" AUTOINCREMENT)
29);
30INSERT INTO professeur VALUES(1,'BIBINNE','Renée');
31INSERT INTO professeur VALUES(2,'MCGONAGALL','Minerva');
32INSERT INTO professeur VALUES(3,'ROGUE','Severus');
33INSERT INTO professeur VALUES(4,'SIBYLLE','Trelawney');
34INSERT INTO professeur VALUES(5,'QUIRINUS','Quirrell');
35INSERT INTO professeur VALUES(6,'RUBEUS','Hagrid');
36INSERT INTO professeur VALUES(7,'DUMBLEDORE','Albus');
37INSERT INTO professeur VALUES(8,'CHOURAVE','Pomona');
38INSERT INTO professeur VALUES(9,'CUTHEBERT','Binns');
39INSERT INTO professeur VALUES(10,'FILIUS','Flitwick');
40INSERT INTO professeur VALUES(11,'SINISTRA','Aurora');
41CREATE TABLE IF NOT EXISTS "cours" (
42 "cours" TEXT NOT NULL UNIQUE,
43 "ects" INTEGER,
44 "professeur_id" INTEGER,
45 "nom" TEXT,
46 PRIMARY KEY("cours")
47);
48INSERT INTO cours VALUES('QUID3001',10,1,'Vol sur balais');
49INSERT INTO cours VALUES('ASTRO6000',10,11,'Astronomie');
50INSERT INTO cours VALUES('BOTA1001',10,8,'Botanique');
51INSERT INTO cours VALUES('DEFE2001',15,5,'Défense contre les forces du mal');
52INSERT INTO cours VALUES('HIST4001',10,9,'Histoire de la magie');
53INSERT INTO cours VALUES('META7001',10,2,'Métamorphose');
54INSERT INTO cours VALUES('POTI9001',10,3,'Potions');
55INSERT INTO cours VALUES('SORTI8001',10,10,'Sortilèges');
56CREATE TABLE IF NOT EXISTS "cours_etudiant" (
57 "inscription_id" INTEGER NOT NULL,
58 "cours" TEXT NOT NULL, note INTEGER,
59 FOREIGN KEY("cours") REFERENCES "cours"("cours"),
60 FOREIGN KEY("inscription_id") REFERENCES "inscription"("inscription_id")
61);
62INSERT INTO cours_etudiant VALUES(4,'POTI9001',9);
63INSERT INTO cours_etudiant VALUES(4,'DEFE2001',17);
64INSERT INTO cours_etudiant VALUES(4,'HIST4001',5);
65
66DELETE FROM sqlite_sequence;
67INSERT INTO sqlite_sequence VALUES('etudiant',78);
68INSERT INTO sqlite_sequence VALUES('inscription',81);
69INSERT INTO sqlite_sequence VALUES('professeur',11);
70COMMIT;
Lancer les tests¶
Voici le fichier complet final test_etudiant.py
:
1import os
2import tempfile
3import unittest
4
5from poudlard import create_app
6from poudlard.db import close_db, get_db
7from poudlard.models.etudiant import get_etudiant_by_maison
8
9class TestUser(unittest.TestCase):
10 def test_search_by_maison(self):
11 # unit test against the test db.
12 maisons_dict = get_etudiant_by_maison()
13 self.assertEqual(len(maisons_dict), 1)
14 maisons = list(maisons_dict.keys())
15 self.assertEqual(len(maisons), 1)
16 self.assertEqual(maisons[0], 'Gryffondor')
17 etudiants = maisons_dict['Gryffondor']
18 self.assertEqual(etudiants[0]['nom'], 'Potter')
19
20 def setUp(self):
21
22 # generate a temporary file for the test db
23 self.db_fd, self.db_path = tempfile.mkstemp()
24 # create the testapp with the temp file for the test db
25 self.app = create_app({'TESTING': True, 'DATABASE': self.db_path})
26 self.app_context = self.app.app_context()
27 self.app_context.push()
28
29 self.db = get_db()
30 # read in SQL for populating test data
31 with open(os.path.join(os.path.dirname(__file__), "schema_test.sql"), "rb") as f:
32 self.db.executescript(f.read().decode("utf8"))
33 def tearDown(self):
34 # closing the db and cleaning the temp file
35 close_db()
36 os.close(self.db_fd)
37 os.unlink(self.db_path)
Pour lancer les tests, on peut utiliser la commande suivante :
$ python3 -m unittest discover -v -s ./poudlard/tests
test_search_by_maison (test_etudiant_model.TestUser) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.026s
OK
On peut aussi utiliser le module coverage
pour voir le pourcentage de code couvert par les tests. Pour cela, on installe le module coverage
avec pip, et on lance les tests avec la commande suivante :
$ coverage run --source ./poudlard/ -m unittest discover -v -s ./poudlard/tests
test_search_by_maison (test_etudiant_model.TestUser) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.027s
OK
Le résultat devrait ressembler à ceci :

Intégration continue¶
Ca serait très pratique si on pouvait lancer tous ces tests à chaque fois que quelqu’un push sur git, et envoyer un email si la branche est « cassée ». C’est ce que fait l’intégration continue.

Dans le pipeline donné dans votre projet, l'étape "tests" lance les tests unitaires. Si un test échoue, le pipeline s'arrête et vous recevez un email. Si tous les tests passent, le pipeline continue.

Si vous cliquez sur l'étape test, vous aurez également directement en ligne le rapport de coverage. Il est également repris sur la partie de droite.