TD I1 : Programmation pilotée par les TESTS¶

!!! info Objectifs de ce travail Découverte du mot clé assert et l'utiliser pour aborder le principe de la programmation pilotée par les tests. Il s'agit d'estimer si une fonction est correcte ou pas. Bien comprendre que l'on ne parle pas d'une démonstration, mais juste d'une bonne methode permettant de se "rassurer" sur le bon fonctionnement d'une fonction. !!!

I. Utilisation raisonnée du mot clé "assert"¶

Q1. Écrire une fonction racine_carree(x : float) -> float dont le but est de calculer la racine carrée du nombre reçu. On prendra soin d'utiliser le mot clé assert pour s'assurer de la validité du nombre reçu.

In [ ]:
# Fonction racine carrée 

import math

# à compléter

Q2. Écrire de même une fonction tangente(x : float) -> float qui renvoie la tangente du nombre reçu x(exprimé en radians). On s'asserera également avec le mot clé assert que le x n'est pas dans l'ensemble $\{ \frac{\pi}{2}+k\pi,\quad k\in\mathbb Z\}$.

In [ ]:
# Fonction tangente

import math

def tangente(x):
    assert # à compléter...
    return math.tan(x)

print(tangente(math.pi+math.pi/2))

II. Programmation par les tests¶

Lors de la conception d'un pogramme, il est une bonne pratique de concevoir pour chaque fonction écrite, un jeu de tests qui permet d'augmenter les chances que celle-ci soit correcte.

Exercice 1 : un exemple trivial¶

Quelqu'un nous a fourni un module nommé mes_fonctions qui contient une fonction double(x : float)-> float qui doit renvoyer le double de la valeur reçue en paramètre. Exécuter les cellules ci-dessous pour obtenir des information via l'aide intégrée :

In [ ]:
import mes_fonctions as mf

dir(mf)
In [ ]:
help(mf.double)

Pour tester la validité de cette fonction, jusqu'à présent on aurait utilisé basiquement la fonction print(...) en faisant quelque chose comme cela:

print(mf.double(3))
print(mf.double(0))
print(mf.double(-1))

Mais l'inconvénient est que si par exemple la fonction donne de temps en temps des bonnes valeurs mais pas tout le temps, il faudra corriger le code de la fonction, puis effectuer à nouveau tous les tests. C'est un peu lourd. L'idée est donc de regrouper tous ces tests dans une fonction dédiée et d'utiliser le mot clé assert plutôt que la fonction print:

!!! warning Principe "first test design" Pour chaque fonction créee, on prendra l'habitude d'écrire en amont une (ou plusieurs) fonction test (dont le nom commence par test_ suivi du nom de la fonction). Cette syntaxe est usuelle, car utilisée par des logiciels tiers qui automatisent leur exécution (mais on ne s'en servira pas cette année...). !!!

Q3. Faire quelques tests via la création d'une fonction test_double() pour voir si la fonction double semble correcte

In [ ]:
# tests sur la fonction double

def test_double():
    # à complter...
    
test_double()

On devrait constater que la fonction double du module mes_fonctions ne fonctionne pas toujours ! Présence du bandeau rouge en sortie, avec la mention AssertionError qui précise en quelle ligne notre test a échoué.

Il faudrait la corriger... (mais inutile ici, c'était juste pour l'exemple !)

Remarque : on aurait aussi pu faire beaucoup plus de tests en comparant notre fonction à une fonction que l'on sait être correcte (ici la simple multiplication par deux). Par exemple ainsi (mais on ne sait plus quelle valeur fait échouer le test...):

In [ ]:
def test_double():
    from random import randint
    for i in range(100):
        x = randint(-50,50)
        assert double(x) == 2*x

test_double()

Exercice 2 : deux fonctions puissances¶

On souhaite écrire une fonction puissance(nombre : float, exposant : int) qui calcule nombre à la puissance exposant. On se contentera d'exposants entiers et positifs ou nuls.

Q4. Commencer par écrire une fonction test_puissance() qui propose quelques tests bien choisis pour voir si nos futures fonctions seront valides.

In [ ]:
def test_puissance():
    # à compléter...
    
    print("Tous les tests sont passés")

Q5. Écrire ensuite une fonction puissance(nombre : float, exposant : int) qui calcule nombre à la puissance exposant. Attention, on demande d'utiliser assert pour n'accepter que des entiers positifs ou nuls pour exposant, et de ne pas utiliser la commande ** pour calculer la puissance...

On appelle bien sûr la fonction test_puissance() pour passer les tests !

In [ ]:
def puissance(nombre,exposant):
    # à compléter....
    
    
test_puissance()

Q6. On considère la fonction mystère ci-dessous. Appliquez-lui les même tests que précédemment pour constater qu'elle fait la même chose.

In [ ]:
def mystere(x,n):
    if n == 0 : 
        return 1
    if n%2 == 0 :
        m = mystere(x,n//2)
        return m*m
    else :
        m = mystere(x,n-1)
        return x*m
In [ ]:
def test_mystere():
    # à compléter par les mêmes tests que dans test_puissance....
    
    print("Tests OK")
    
test_mystere()

Q7. Donner ci-dessous une version améliorée de votre fonction test pour qu'elle teste la fonction puissance(n,e) sur tous les nombres entiers $n$ entre -10 et 10 à toutes les puissances $e$ entre 0 et 100 (besoin de boucles imbriquées...) en comparant les résultats à ceux obtenus avec l'opérateur python **.

In [ ]:
def test_puissance():
    # à compléter...
    
    print("Tous les tests sont passés")
    
test_puissance()

Exercie 3 : « refactoring » du tri par sélection¶

Il y a beaucoup de façon différentes pour trier une liste. Voyons ici un algorithme intuitif, le tri par sélection. Le principe est de parcourir plusieurs fois la liste, repérer à chaque fois le plus petit et faire une permutation. Voir cette vidéo youtube. On donne ci-dessous un algorithme brut qui implémente cette façon de trier. Le lire attentivement et bien faire le parallèle avec la vidéo.

In [ ]:
def selection(tab):
    tab1 = tab.copy()
    for i in range(len(tab1) - 1):
        min_index = i
        for j in range(i + 1, len(tab1)):
            if tab1[j] < tab1[min_index]:
                min_index = j
        tab1[i], tab1[min_index] = tab1[min_index], tab1[i]
    return tab1

L'objectif de cet exercice est de refactoriser cette fonction, c'est à dire la découper en fonctions élémentaires afin d'améliorer la lisibilité du code. On identifie en effet deux tâches élémentaires :

  • repérer l'indice du plus petit élément dans une liste, à partir d'un certain indice,
  • permuter deux élements de la liste dont on connait les indices.

Q9. Écrire une fonction test_selection() qui permet de tester sur quelques exemples si la fonction selection semble correcte.

In [ ]:
def test_selection():
    # à compléter....
    

test_selection()

Maointenant que l'on dispose d'une fonction de test, on peut se lancer dans la modification de la fonction selection et tester à chaque modification...

Q10. Écrire une fonction permuter(tab,i,j) qui renvoie une copie de la liste tab dont les éléments d'indice i et j ont été permuté. Bien entendu, on doit aussi écrire une fonction de test associée.

In [ ]:
def test_permuter():
    # à compléter....
    
def permuter(tab : list, i : int, j : int) :
    # compléter avec la docstring....
    
    tab1 = tab.copy()
    # à compléter....
    return tab1

test_permuter()

Q11. Écrire une fonction min_indice_tableau(tab,i) qui doit renvoyer l'indice du plus petit élément du tableau tab à partir de l'indice i. Penser à la focntion test associée.

In [ ]:
def test_min_indice_tableau():
    # à compléter
    
def min_indice_tableau(tab,i):
    # à compléter (docstring et code...)
    
test_min_indice_tableau()

Q12. Réécrire enfin la fonction selection(tab) en utilisant les deux fonctions permuter et min_indice_tableau.

In [ ]:
def selection(tab):
    # compléter par docstring et code ....
    
test_selection()