.. _ref-projet3: ================================================= Chapitre 11 : Projet phase 3, les tests unitaires ================================================= Ce chapitre présente la phase 3 du projet. Il se basera sur la phase 2, donc sur l'architecture git, et le site web en Flask utilisant une base de données SQL. Les deux premières semaines (S6 -> S7), vous implémenterez des tests unitaires comme qu'expliqué ci-dessous. Les deadlines précises sont communiquées dans les slides et sur Moodle. La troisième semaine (S8), vous ferez une revue par les pairs (peer-review), c'est-à-dire la relecture du travail d'un autre groupe afin de leur donner des commentaires pertinents et de tester leur code et ses limites. Les commentaires sont à rendre une semaine plus tard. Comme d'habitude, chaque groupe **DOIT** réaliser son travail sur son répertoire dédié sur la `Forge UCLouvain `__ qui utilise la plateforme GitLab et le gestionnaire de version Git. Ceci permettra de garder une trace des contributions des différents membres du groupe. Pensez à organiser votre Git proprement et utilisez des messages de commit clairs. Par ailleurs, toute intégration à la branche *main* doit se faire avec des merge requests, relues par au moins un condisciple. Une application testée ---------------------- On vous demande d'implémenter et d'intégrer à votre application web la fonctionnalité suivante : - Donner la possibilité de sélectionner un vol et d'en afficher l'émission CO2 estimée ainsi que la distance à vol d'oiseau entre le départ et l'arrivée. On vous demande aussi d'intégrer des tests pour cette fonctionnalité ainsi que pour les requêtes de la phase précédente. Fonctions ========= Pour ce faire, vous devrez implémenter deux fonctions suivantes dans un fichier dédié ``emissions.py``: Distance -------- ``distance(lat_from: Decimal, long_from: Decimal, lat_to: Decimal, long_to: Decimal) -> Decimal`` : qui étant donné les coordonnées de l'aéroport de départ ``to`` et de celui d'arrivée ``from`` **en radian** retourne une distance en kilomètre. Calculer une distance entre deux points sur Terre de façon précise n'est pas si aisé. Pour nous simplifier la tâche, nous allons utilisez l'approximation `suivante `__. La formule à utiliser est : ``D = R*(arcos(sin(lat_from)*sin(lat_to)+cos(lat_from)*cos(lat_to)*(cos(long_from-long_to))))`` R est le rayon de la Terre qui est de `6378 KM`. Les fonctions trigonométriques se trouvent dans la librairie **math**, vous pouvez les utiliser directement, en revanche, vous ne pouvez pas utiliser de librairie qui calcule pour vous la distance entre deux points sur Terre. **Attention:** Dans la base de données, les angles sont donnés en dégrés et non en radiant, pour convertir les degrés en radiants, vous devez utiliser la fonction ``radians`` dans la librairie `math `__. En Python : math.radians(degree) Si par exemple, nous calculons la distance ``CRL(Brussels South Charleroi Airport)-TNG(Tangier Ibn Battouta Airport)`` donc la distance entre ``(50.46,4.45)`` et ``(35.7,-5.9)`` dans la base de données. En utilisant la formule, nous obtenons `1842 KM`. `www.airmilescalculator.com` nous donne la valeur de `1838 KM` pour le même vol, pas mal ! Comme la précision de cet algorithme dépend de la précision de l'arithmétique de nombres décimaux, vous devrez utiliser `decimal.Decimal `__ pour représenter les valeurs numériques. Emission -------- ``emission(distance: Decimal, aircraft: AirCraft) -> Decimal`` : qui était données la distance ainsi que le type de l'avion vous donnerons les émissions en tonnes de CO2 du vol. Commençons par déterminer les éléments qui nous permettront de calculer les émissions de CO2 équivalent d'un vol, pour simplifier, nous pouvons considérer qu'il existe 3 catégories d'avions qui dans la base de données seront représentés par ``S`` : small, ``M`` : medium, ``H`` Heavy et ``J`` Jumbo. Nous considérerons qu'ils consomment respectivement, 1, 2.5, 5 et 12 Tonnes de carburant/Heure. Ensuite, pour calculer la quantité CO2 équivalent émit, nous devons multiplier la quantité de carburant par 3.15 pour obtenir la quantité de CO2 relâché dans l'atmosphère. De plus, la combustion de carburant émet d'autres gaz en plus du CO2. Nous pouvons ajouter un facteur 2 au résultat obtenu pour avoir l'émission de CO2 équivalent. Nous allons également considérer que la vitesse moyenne d'un avion est de 800 km/h. Nous nous intéresserons uniquement à l'émission CO2 d'un vol, et pas à celui d'un passager. Un même avion peut avoir des configurations de sièges différentes et le taux d'occupation peut varier ce qui compliquerait davantage le calcul. Si nous prenons un vol CRL->TNG avec une distance de 1838KM comme calculé précédemment et un avion de type ``H``, nous pouvons faire le calcul suivant. Durée : 2.3H (1838KM/800KM/H) Consommation 5T/H (appareil ``H``) Résultat = (5*2.3*3.15*2) = 72.45 Nous avons donc 72.45 tonnes de CO2 pour tout le vol. Voici un squelette, complétez-le en implémentant les fonctions ``distance`` et ``emission``. .. container:: literal-block-wrapper docutils :name: idemission .. container:: code-block-caption ``mobility/emission.py`` .. code-block:: python from enum import Enum import math from decimal import Decimal class AirCraft(Enum): L = 0 M = 1 H = 2 J = 3 def distance(lat_from: Decimal, long_from: Decimal, lat_to: Decimal, long_to: Decimal) -> Decimal: return 0 def emission(distance: Decimal, aircraft: AirCraft) -> Decimal: return 0 Page ==== Vous créerez une nouvelle page "Emissions" sur le site qui permettra d'afficher tous les *départs* de nos 5 plus grands aéroports nationaux le premier Janvier 2025. Il y aura donc 5 tables avec tous les vols, comme vous le verriez à l'aéroport lui-même. La table affichera la destination, la distance, et bien évidemment l'émissions pour ce vol. A la fin chaque table vous ferez le bilan carbonne de la journée. Tests unitaires =============== On vous demande aussi de fournir une classe de tests qui testera la logique de ces différentes méthodes. Pour ce faire, on vous demande d'utiliser le module `unittest `__ comme vu en LSINF1101 et/ou comme rappelé dans le syllabus et au cours. Vous devrez créer vos fichiers de tests dans un sous-répertoire ``tests`` et ils devront respecter la nomenclature ``test_*.py`` où ``*`` correspond à la classe ou au module testé pour que unittest les découvre automatiquement. Une classe de tests unitaires hérite de ``unittest.TestCase`` et porte un nom logique ``*TestCase`` correspondant à la classe ou au module testé. Chaque classe teste une fonctionnalité précise et chaque méthode de test dans la classe sera nommée en commençant par ``test_`` comme ``def test_foo(self) :`` contenant une série d'assertions qui vérifient que pour un input connu, l'output produit est celui attendu (expected). Un squelette d'une telle classe ressemblera donc à ceci. .. container:: literal-block-wrapper docutils :name: idtestemission .. container:: code-block-caption ``tests/test_emission.py`` .. code-block:: python import unittest import mobility.emission class EmissionTestCase (unittest.TestCase): def test_distance(self): # Un angle de 1 Radian intercept un arc de longueur égal au rayon # Nous prenons deux points distants d'un angle de 1 radian expected = 6378 a = (0, 1) b = (0, 2) actual = mobility.emission.distance(a[0], a[1], b[0], b[1]) self.assertAlmostEqual(actual, expected, 1, msg=f'My error message on pl.f({actual}) different than {expected}') def test_emission(self): # TODO # C'est à vous de jouer ici self.assertTrue(True) if __name__ == '__main__': unittest.main(verbosity=2) Cette classe doit être enrichie et couvrir un maximum de ``emission`` (via l'utilisation de ``coverage``). Pour exécuter vos tests, vous pouvez depuis la racine du projet lancer la commande suivante : ``$ python3 -m unittest discover -v -s ./mobility/tests`` Ensuite, nous voulons que vous testiez les fonctionnalités liées aux sélections filtrées dans la base de données afin de vous assurer que vos requêtes soient correctes. Vous devez tester toutes les requêtes implémentées pour la phase 2 du projet. De tels tests sont appelés tests d'intégration, car ils font intervenir plusieurs composants de votre application (notamment ici l'app et la DB) par opposition aux tests unitaires qui vont tester la fonctionnalité d'une méthode ou une classe dont la logique est limitée. Voici un template d'une classe permettant de faire de tels tests sur une DB de test. L'utilisation d'une DB de test est importante afin de ne pas risquer d'interférer avec la base de données utilisée en production, mais aussi pour pouvoir raisonner sur une base de données plus petite ou les résultats attendus sont plus faciles à calculer. Pour créer cette DB de test, nous utiliserons un petit script SQL proche de ceux que nous avons utilisés lors du second TP Flask. Le code pour générer les différentes tables vous est fourni dans :ref:`ref-projet2`. .. container:: literal-block-wrapper docutils :name: id1 .. container:: code-block-caption ``mobility/tests/schema_test.sql`` .. code-block:: sql PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; DROP TABLE IF EXISTS country; CREATE TABLE country(iso_country TEXT PRIMARY KEY, name TEXT); INSERT INTO country(iso_country,name) VALUES("FI", "Finland"); INSERT INTO country(iso_country,name) VALUES("FR", "France"); COMMIT; Voici ci-dessous un exemple de test faisant intervenir la base de données. .. container:: literal-block-wrapper docutils :name: id1 .. container:: code-block-caption ``mobility/tests/test_example.py`` .. code-block:: python import os import tempfile import unittest import json from mobility import create_app from mobility.db import close_db, get_db from mobility.models.country import get_country_list class TestUser(unittest.TestCase): def test_country_list(self): # unit test against the test db. expected_countries = [ {"iso_country": "FI", "name": "Finland"}, {"iso_country": "FR", "name": "France"} ] # easy way to convert sqlite3.Row to dict actual_countries = [dict(row) for row in get_country_list()] self.assertEqual(len(actual_countries), 2) for i in range(len(expected_countries)): actual_country = actual_countries[i] expected_country = expected_countries[i] self.assertEqual(actual_country, expected_country) 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) Notez que la méthode `setUp `__ est héritée de ``unittest.TestCase``. Elle est exécutée avant chaque test et permet de travailler avec une DB de test neuve à chaque fois. Ici, une instance de l'app avec une DB de test stockée dans un fichier temporaire est créée pour chaque test. La méthode `tearDown `__ est, elle aussi, héritée de ``unittest.TestCase`` et permet de nettoyer la db après chaque test. Les tests seront finalement automatiquement découverts par unittest. Vous pouvez les lancer depuis le terminal dans le venv en supposant que vous soyez à la racine du projet avec la commande suivante : ``$ python -m unittest discover -v -s ./mobility/tests``. Correction du code ================== Il n'y a pas de rapport pour cette phase. Le principal retour se fera par peer-review (voir ci-dessous). Etant donné qu'il n'y a pas de rapport, la correction se fera rapidement à la réunion de suivi de S8, vous corrigerez avec le tuteur le projet comme suit. Pour la deadline, Dimanche de S7 à 18H, vous devez juste pousser un tag "phase3" et vous assurer que le site est en ligne. Site ---- * Le site fonctionne et est en ligne. 1 * Les nouvelles fonctionnalités sont intégrées 3 Tests ----- * Il existe au moins 1 test d'intégration interagissant avec la DB 2 * Il existe une DB de test (utilisée correctement) 1 * Le coverage est suffisant 2 * Distance est testé dans plusieurs cas 1 * Emission est testé dans plusieurs cas 1 Notez que la note des critères est indicative uniquement. Pour rappel, la phase 3 elle-même compte pour 5% de la note finale car c'est une phase intermédiaire, sans rapport. Review ====== En fin de S7, le Dimanche à 18H (voir au début du chapitre), vous remettrez un zip du code source de l'application sur Moodle dans l'activité "Review" et l'enverrez à deux groupes. Une fois tous les rapports remis, l'attribution de deux rapports à chaque groupe se fera automatiquement le lundi matin. Si votre code n'était pas envoyé dans l'activité à temps, vous aurez 0 à la review car nous ne pouvons pas retarder tout le monde pour un seul retard. Vous aurez ensuite une semaine pour faire les reviews. Vous devez ensuite corriger la phase 3 en prenant en compte les commentaires des deux reviews reçues. Notez que le système de review sur Moodle est nouveau. Il a été testé mais pourrait contenir des bugs. En cas de problème postez un message sur le forum. L'outil devrait vous guider. Comment faire une bonne review ------------------------------ Lors de la phase de peer-review du code, nous vous demandons de vérifier la qualité et la structure du code de l'application d'un autre goupe d'étudiants ainsi que de la suite de test. Guidez-vous en répondant aux questions suivantes : - Le code de l'application respecte-t-il la structure proposée ? - Les différentes fonctionalités sont-elles dans des fichiers séparés ? - Le code python proposé est-il clair et bien documenté ? - Les noms des variables sont-ils bien choisis ? - Les commentaires sont-ils appropriés et clairs ? - Les tests montrent-ils qu'ils ont été écrits par des étudiants qui maîtrisent le langage python (utilisation de fonctions, éviter d'avoir trop de code dupliqué, même si dans des tests unitaires il y a plus souvent du code dupliqué que dans le code normal) - Les test sont-ils complets et corrects ? Si non, proposez dans votre rapport d'éventuels cas de test supplémentaires en justifiant leur utilité. Cette analyse prend du temps, mais c'est important que vous appreniez à lire du code écrit par d'autres informaticiens. Dans votre vie professionnelle, vous devrez beaucoup plus souvent modifier des programmes écrits par d'autres informaticiens que de développer des programmes à partir de rien. Il est aussi important que vous appreniez à écrire des commentaires constructifs qui permettent à ceux qui les lisent d'améliorer leur travail. Évitez les formulations négatives ou blessantes. Relisez votre review en vous mettant à la place du groupe receveur et demandez-vous si elle vous semble utile et constructive. La review que vous écrivez dans le cadre de ce projet compte pour 5% de la cote finale, et la participation dans l'évaluation de groupe. Vous avez donc tout intérêt à la faire correctement. Par contre, votre review n'influence pas la côte qui sera attribuée aux groupes que vous évaluez. Vous les aidez à améliorer leur travail et à obtenir une meilleure note mais vous ne leur attribuez pas de note. Une review de qualité contient les éléments suivants : - Au moins deux commentaires positifs sur le travail effectué. Cela comprend : - au moins un commentaire sur le fond, - au moins un commentaire sur la forme. - Au moins deux suggestions d'améliorations possibles. Cela comprend : - au moins une suggestion sur le fond, et/ou une suggestion de test unitaire à ajouter pour compléter la suite de test, - au moins une suggestion sur la forme. Évidemment, il s'agit là de l'attente minimale. Vous êtes vivement encouragés à apporter plus d'informations dans votre review, en suivant les conseils plus haut sur cette page ! Correction de la review ----------------------- Les deux reviews comptent pour 5% de la cote finale. * La structure de l'application est mentionnée 1 * La séparation du code en fichiers est commentée 1 * La clareté du code (variable, commentaires) est mentionnée 1 * La qualité des tests, leurs complétude, et le coverage est discuté 2 * Les fonctionnalités sont évaluées : * Distance 1 * Emission 1 * page 1 * La review comprends au moins un commentaire positif 1 * La review comprends au moins une piste d'amélioration 2 Notez que la note des critères est indicative uniquement.