Tests unitaires

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.

[1]:
def unit_test():
    y = f(x)
    if y != valeur_attendue:
        raise AssertionError(f"{y} != {valeur_attendue}")

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.

[2]:
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.

[3]:
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.

[4]:
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.

[5]:
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)])
[5]:
4.0
[ ]:

[ ]:

[6]:
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 <répertoire> 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

[7]:
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  # ...
[8]:
def test_tour_prend_piece_obstacle_exception():
    # ...
    pass
[ ]:

[ ]:


Notebook on github