.. _2022unittestrst: ============== Test unitaires ============== .. only:: html **Links:** :download:`notebook <2022_unit_test.ipynb>`, :downloadlink:`html <2022_unit_test2html.html>`, :download:`python <2022_unit_test.py>`, :downloadlink:`slides <2022_unit_test.slides.html>`, :githublink:`GitHub|_doc/notebooks/td1a_home/2022_unit_test.ipynb|*` Les tests unitaires sont l’élément clé pour créer un programme fiable. Il est impensable de s’en passer. Un test unitaire est une fonction qui s’assure qu’une autre fonction retourne le résultat souhaité pour les mêmes entrées. Ils sont présents dans tous les langages. Les modules python les plus utilisés sont aussi les plus testés, ils sont validés par des milliers de tests unitaires. .. code:: ipython3 def unit_test(): y = f(x) if y != valeur_attendue: raise AssertionError(f"{y} != {valeur_attendue}") .. code:: ipython3 from jyquickhelper import add_notebook_menu add_notebook_menu() .. contents:: :local: Un petit jeu ------------ On suppose qu’une tour est placée sur un échiquier, on veut savoir combien de coups il faut pour atteindre une autre case. .. code:: ipython3 def tour_prend_piece(x1, y1, x2, y2): # ... return 1 or 2 Si votre fonction est bien correct, la fonction suivante doit s’exécuter sans erreur. .. code:: ipython3 def test_tour_prend_piece(): assert tour_prend_piece(0, 0, 0, 1) == 1 assert tour_prend_piece(0, 0, 1, 0) == 1 assert tour_prend_piece(1, 0, 0, 0) == 1 assert tour_prend_piece(0, 1, 0, 0) == 1 assert tour_prend_piece(0, 0, 1, 1) == 2 assert tour_prend_piece(0, 2, 1, 1) == 2 Une autre écriture ------------------ La fonction précédente a quatre arguments. On souhaite les remplacer par deux tuple. .. code:: ipython3 def tour_prend_piece_tuple(t1, t2): # ... return True or False def test_tour_prend_piece_tuple(): def _tour_prend_piece(x1, y1, x2, y2): return tour_prend_piece_tuple((x1, y1), (x2, y2)) assert _tour_prend_piece(0, 0, 0, 1) == 1 assert _tour_prend_piece(0, 0, 1, 0) == 1 assert _tour_prend_piece(1, 0, 0, 0) == 1 assert _tour_prend_piece(0, 1, 0, 0) == 1 assert _tour_prend_piece(0, 0, 1, 1) == 2 assert _tour_prend_piece(0, 2, 1, 1) == 2 Des obstacles… -------------- Ecrire une fonction qui prend en compte les obstacles : la tour ne peut pas traverser une case si une pièce est présente. On pourra s’inspirer d’un algorithme de coloriage. Qu’en est-il des tests unitaires précédents ? L’idée est colorier l’échiquier avec le nombre de coups qu’il faut pour atteindre une case. .. code:: ipython3 import numpy def find_neighbour(echiquier, p): x, y = p if x > 0 and echiquier[x-1, y] == -1: return (x-1, y), (-1, 0) if x < echiquier.shape[0] - 1 and echiquier[x+1, y] == -1: return (x+1, y), (1, 0) if y > 0 and echiquier[x, y-1] == -1: return (x, y-1), (0, -1) if y < echiquier.shape[1] - 1 and echiquier[x, y+1] == -1: return (x, y+1), (0, 1) return None, None def coloring(t1, t2, obstacles): obstacles = set(obstacles) # pour aller plus vite echiquier = numpy.zeros((8, 8)) -1 for obs in obstacles: echiquier[obs] = -2 echiquier[t1] = 0 cases = [t1] while len(cases) > 0: case = cases[0] next_case, direction = find_neighbour(echiquier, case) if next_case is None: del cases[0] continue x, y = next_case value = echiquier[case] + 1 while x >= 0 and y >= 0 and x < echiquier.shape[0] and y < echiquier.shape[1]: if echiquier[x, y] == -2: break if echiquier[x, y] == -1: echiquier[x, y] = value else: echiquier[x, y] = min(value, echiquier[x, y]) cases.append((x, y)) x += direction[0] y += direction[1] return echiquier[t2] coloring((0, 0), (6, 6), [(0, 6), (6, 0), (1, 5), (5, 1), (5, 6), (5, 5), (6, 5)]) .. parsed-literal:: 4.0 .. code:: ipython3 def tour_prend_piece_obstacle(t1, t2, obstacles): # ... return # ... Ajouter d’autres tests unitaires pour cette seconde version ----------------------------------------------------------- Changer la taille de l’échiquier -------------------------------- On considère que l’échiquier est de taille connue mais plus nécessairement 8x8. Modifier la fonction pour prendre en compte ce changement. Qu’en est-il des tests unitaires. Pour aller plus loin -------------------- Les tests unitaires : - Ils sont des fonctions sans arguments dont le nom commencent par ``test_``. - Ils sont indispensables quand on travaille à plusieurs : ils assurent que quelqu’un ne *casse* pas votre fonction. - Ils s’écrivent rarement dans un notebook. On les écrit dans un fichier à part, et ils testent des fonctions écrites dans d’autres fichiers mais pas dans des notebooks. - Les tests unitaires doivent être rapides : ils sont exécutés très souvent, ils doivent être courts et rapides. - On teste des résultats numériques mais aussi qu’une fonction crée une exception, un warning… Des milliers de tests unitaires : - `unittest `__ : module python dédiés aux tests unitaires - `pytest `__ : c’est une librairie très utilisées. La commande ``pytest `` cherche toutes les fonctions commencençant par ``test_`` et les exécute. Intégration continue : Exemple avec `scikit-learn `__, résultats des tests `scikit-learn/build `__ Tester une exception -------------------- .. code:: ipython3 def tour_prend_piece_obstacle(t1, t2, obstacles): if min(t1) < 0 or min(t2) < 0: raise ValueError(f"Une pièce est en dehors de l'échiquier, pièces : {t1} ou {t2}.") return # ... .. code:: ipython3 def test_tour_prend_piece_obstacle_exception(): # ... pass