Mokona Guu Center

Test Driven Development : retour sur une erreur

Publié le

Dans l'épisode « Mouvement et Capture » une erreur s'est glissée dans le programme. Je reviens un peu sur cette erreur, car elle donne une indication forte de ce qu'apporte le TDD et ce qu'il ne promet pas.

Un lecteur (lien cassé) m'a fait remarquer qu'il ne comprenait pas la ligne indiquée ci-dessous :

    def capture(self, from_piece_location, to_piece_location):
        from_piece, from_location = from_piece_location
        to_piece, to_location = to_piece_location

        self.game.pieces_on_board[from_piece] = to_location
        self.game.pieces_on_board[to_piece] = to_piece

    def test_allows_a_pawn_to_capture_an_elephant(self):
        from_location = (0, 1)
        to_location = (1, 1)

        from_piece = PIECE_PAWN
        to_piece = PIECE_ELEPHANT

        self.game.drop(from_piece, from_location)
        self.game.drop(to_piece, to_location)

        self.capture((from_piece, from_location), (to_piece, to_location))

        self.assertEqual(PIECE_NONE, self.game.get_piece_at(from_location))
        self.assertEqual(from_piece, self.game.get_piece_at(to_location))

En effet, ça n'a pas de sens. Le tableau associatif a pour clés des pièces de jeu et pour valeurs des emplacements. Or là, si la clé est bien une pièce, la valeur est... la pièce aussi.

Et pourtant, les tests passent. Pendant un moment, il y avait donc un bug qui n'a pas été repéré.

Première chose à comprendre : si les tests passaient, c'est que le programme remplissait bien le contrat décrit par les tests. Le programme est donc fonctionnellement correct.

On peut l'analyser. La valeur étant une pièce, ce n'est pas un emplacement. Toutes les fonctions qui gèrent les valeurs d'emplacement l'ignorent. Comme par exemple :

    def _get_piece_at_location(self, location):
        pieces_at_location = [p[0] for p in self.pieces_on_board.items() if p[1] == location]
        if len(pieces_at_location) == 1:
            return pieces_at_location[0]
        return None

Seconde chose : le tableau associatif est néanmoins incohérent. Ça peut ou non être ennuyeux. Ici, ça ne l'est pas. Dans le programme final, il y aura de toute façon une valeur associée à chaque clé.

Cependant, cela peut être beaucoup plus ennuyeux dans un programme avec des valeurs qui rempliraient un tableau sans être jamais collectées. Intuitivement, on sent qu'il peut y avoir des soucis de mémoire.

D'où vient l'erreur ?

L'erreur vient d'un copier/coller raté lors de l'extraction d'une fonction. Mes deux tests n'étaient pas tout à fait symétrique et lorsque j'ai factorisé le code, j'ai recopié de travers.

Le TDD ne pourra pas vous aider ici si le programme reste fonctionnel. Si par contre l'erreur brisait un comportement, alors les tests auraient signalé le problème.

Peut-on éviter l'erreur ?

La pratique du TDD n'invalide pas d'autres bonnes pratiques comme la « peer review ». Ici, j'écris les articles seul, et leur publication sert de revue de code.

Il est aussi possible, lorsque l'on trouve et on résout ce genre de bug, d'ajouter un test qui révèle l'erreur avant de corriger. En TDD pur, ça serait même essentiel, puisqu'on ne peut pas toucher au code si un test n'est pas en train de signaler un problème.

En pratique, je me suis contenté de corriger mon erreur de copier/coller et vérifier que les tests passent toujours, garantissant que le programme est toujours fonctionnel tels que décrits par les tests. Je n'avais pas envie d'ajouter un test de cohérence sur les types.

J'aurais pu aussi utiliser un langage typé plus fortement que le Python.