Comment TDD une implémentation de liste en Java

Comment TDD une implémentation de liste en Java

 

1. Vue d'ensemble

Dans ce didacticiel, nous allons parcourir une implémentation personnalisée deList à l'aide du processus de développement piloté par les tests (TDD).

Il ne s’agit pas d’une introduction à TDD. Nous supposons donc que vous avez déjà une idée de base de ce que cela signifie et de l’intérêt soutenu pour s’améliorer.

En termes simples,TDD is a design tool, enabling us to drive our implementation with the help of tests.

Un avertissement rapide - nous ne nous concentrons pas sur la création d'une mise en œuvre efficace ici - juste en l'utilisant comme excuse pour afficher les pratiques TDD.

2. Commencer

Tout d'abord, définissons le squelette de notre classe:

public class CustomList implements List {
    private Object[] internal = {};
    // empty implementation methods
}

La classeCustomList implémente l'interfaceList, elle doit donc contenir des implémentations pour toutes les méthodes déclarées dans cette interface.

Pour commencer, nous pouvons simplement fournir des corps vides pour ces méthodes. Si une méthode a un type de retour, nous pouvons renvoyer une valeur arbitraire de ce type, telle quenull pourObject oufalse pourboolean.

Par souci de brièveté, nous omettons les méthodes facultatives, ainsi que certaines méthodes obligatoires qui ne sont pas souvent utilisées.

3. Cycles TDD

Développer notre implémentation avec TDD signifie que nous devonscreate test cases first, définissant ainsi les exigences de notre implémentation. Seulementthen we’ll create or fix the implementation code pour que ces tests réussissent.

De manière très simplifiée, les trois principales étapes de chaque cycle sont les suivantes:

  1. Writing tests – définissent les exigences sous forme de tests

  2. Implementing features – font passer les tests sans trop se focaliser sur l'élégance du code

  3. Refactoring – améliore le code pour le rendre plus facile à lire et à maintenir tout en réussissant les tests

Nous allons parcourir ces cycles TDD pour certaines méthodes de l'interfaceList, en commençant par les plus simples.

4. La méthodeisEmpty

La méthodeisEmpty est probablement la méthode la plus simple définie dans l'interfaceList. Voici notre mise en œuvre de départ:

@Override
public boolean isEmpty() {
    return false;
}

Cette définition de méthode initiale est suffisante pour compiler. Le corps de cette méthode sera «obligé» de s'améliorer lorsque de plus en plus de tests seront ajoutés.

4.1. Le premier cycle

Écrivons le premier cas de test qui garantit que la méthodeisEmpty renvoietrue lorsque la liste ne contient aucun élément:

@Test
public void givenEmptyList_whenIsEmpty_thenTrueIsReturned() {
    List list = new CustomList<>();

    assertTrue(list.isEmpty());
}


Le test donné échoue car la méthodeisEmpty renvoie toujoursfalse. Nous pouvons le faire passer simplement en retournant la valeur de retour:

@Override
public boolean isEmpty() {
    return true;
}

4.2. Le deuxième cycle

Pour confirmer que la méthodeisEmpty renvoiefalse lorsque la liste n’est pas vide, nous devons ajouter au moins un élément:

@Test
public void givenNonEmptyList_whenIsEmpty_thenFalseIsReturned() {
    List list = new CustomList<>();
    list.add(null);

    assertFalse(list.isEmpty());
}


Une implémentation de la méthodeadd est désormais requise. Voici la méthodeadd avec laquelle nous commençons:

@Override
public boolean add(E element) {
    return false;
}

Cette mise en œuvre de méthode ne fonctionne pas car aucune modification de la structure de données interne de la liste n'est effectuée. Mettons-le à jour pour stocker l'élément ajouté:

@Override
public boolean add(E element) {
    internal = new Object[] { element };
    return false;
}

Notre test échoue toujours car la méthodeisEmpty n'a pas été améliorée. Faisons cela:

@Override
public boolean isEmpty() {
    if (internal.length != 0) {
        return false;
    } else {
        return true;
    }
}

Le test non vide réussit à ce stade.

4.3. Refactoring

Les deux cas de test que nous avons vus jusqu'à présent réussissent, mais le code de la méthodeisEmpty pourrait être plus élégant.

Refactorisons-le:

@Override
public boolean isEmpty() {
    return internal.length == 0;
}

Nous pouvons voir que les tests réussissent, donc l'implémentation de la méthodeisEmpty est maintenant terminée.

5. La méthodesize

Voici notre implémentation de départ de la méthodesize permettant à la classeCustomList de compiler:

@Override
public int size() {
    return 0;
}

5.1. Le premier cycle

En utilisant la méthodeadd existante, nous pouvons créer le premier test pour la méthodesize, en vérifiant que la taille d'une liste avec un seul élément est1:

@Test
public void givenListWithAnElement_whenSize_thenOneIsReturned() {
    List list = new CustomList<>();
    list.add(null);

    assertEquals(1, list.size());
}


Le test échoue car la méthodesize renvoie0. Faisons-le passer avec une nouvelle implémentation:

@Override
public int size() {
    if (isEmpty()) {
        return 0;
    } else {
        return internal.length;
    }
}

5.2. Refactoring

Nous pouvons refactoriser la méthodesize pour la rendre plus élégante:

@Override
public int size() {
    return internal.length;
}

L'implémentation de cette méthode est maintenant terminée.

6. La méthodeget

Voici l'implémentation de départ deget:

@Override
public E get(int index) {
    return null;
}

6.1. Le premier cycle

Jetons un coup d'œil au premier test de cette méthode, qui vérifie la valeur de l'élément unique de la liste:

@Test
public void givenListWithAnElement_whenGet_thenThatElementIsReturned() {
    List list = new CustomList<>();
    list.add("example");
    Object element = list.get(0);

    assertEquals("example", element);
}


Le test passera avec cette implémentation de la méthodeget:

@Override
public E get(int index) {
    return (E) internal[0];
}

6.2. Amélioration

Habituellement, nous ajoutons d'autres tests avant d'apporter des améliorations supplémentaires à la méthodeget. Ces tests auraient besoin d'autres méthodes de l'interfaceList pour implémenter les assertions appropriées.

Cependant, ces autres méthodes ne sont pas encore assez matures, donc nous rompons le cycle TDD et créons une implémentation complète de la méthodeget, ce qui n'est en fait pas très difficile.

Il est facile d’imaginer queget doit extraire un élément du tableauinternal à l’emplacement spécifié à l’aide du paramètreindex:

@Override
public E get(int index) {
    return (E) internal[index];
}

7. La méthodeadd

C'est la méthodeadd que nous avons créée dans la section 4:

@Override
public boolean add(E element) {
    internal = new Object[] { element };
    return false;
}

7.1. Le premier cycle

Voici un test simple qui vérifie la valeur de retour deadd:

@Test
public void givenEmptyList_whenElementIsAdded_thenGetReturnsThatElement() {
    List list = new CustomList<>();
    boolean succeeded = list.add(null);

    assertTrue(succeeded);
}


Nous devons modifier la méthodeadd pour renvoyertrue pour que le test réussisse:

@Override
public boolean add(E element) {
    internal = new Object[] { element };
    return true;
}

Bien que le test réussisse, la méthodeadd ne couvre pas encore tous les cas. Si nous ajoutons un deuxième élément à la liste, l'élément existant sera perdu.

7.2. Le deuxième cycle

Voici un autre test ajoutant l'exigence selon laquelle la liste peut contenir plus d'un élément:

@Test
public void givenListWithAnElement_whenAnotherIsAdded_thenGetReturnsBoth() {
    List list = new CustomList<>();
    list.add("example");
    list.add(".com");
    Object element1 = list.get(0);
    Object element2 = list.get(1);

    assertEquals("example", element1);
    assertEquals(".com", element2);
}


Le test échouera car la méthodeadd dans sa forme actuelle ne permet pas d'ajouter plus d'un élément.

Modifions le code de mise en œuvre:

@Override
public boolean add(E element) {
    Object[] temp = Arrays.copyOf(internal, internal.length + 1);
    temp[internal.length] = element;
    internal = temp;
    return true;
}

La mise en œuvre est assez élégante, nous n’avons donc pas besoin de la refactoriser.

8. Conclusion

Ce didacticiel a suivi un processus de développement piloté par les tests pour créer une partie d'une implémentation deList personnalisée. En utilisant TDD, nous pouvons implémenter les exigences étape par étape, tout en maintenant la couverture de test à un niveau très élevé. En outre, l'implémentation est testable, car elle a été créée pour réussir les tests.

Notez que la classe personnalisée créée dans cet article est uniquement utilisée à des fins de démonstration et ne doit pas être adoptée dans un projet réel.

Le code source complet de ce didacticiel, y compris les méthodes de test et d'implémentation laissées de côté par souci de brièveté, se trouve àover on GitHub.