mardi 12 octobre 2010

DSL pour les tests unitaires

J'ai lu plusieurs articles sur la notion de D.S.L. (Domain Specific Language), et notamment pour les tests unitaires. L'idée étant de développer son propre "langage" pour améliorer la lisibilité du code.

L'utilité des tests unitaires ne fait plus aucun doute, du moins pour moi ... Mais je me suis aperçu qu'il n'est pas évident, pour tous les développeurs, que le code pour les tests unitaires doit être aussi soigné que le code de prod : écriture, présentation, conception, etc ... Pourtant, pour garder leur intérêt, les tests unitaires doivent évoluer en même temps que le code de prod. Ce code de test doit donc être très facilement accessible et compréhensible par un développeur qui le découvre et doit le maintenir, sinon c'est ... @Ignore assuré ! ... :-(

Partant de là, le DSL me semble intéressant et rien de tel qu'un exemple. Désolé pour cet article un peu long, mais je souhaite être précis et explicite.

Utilisation d'une API immutable


Imaginons que j'ai à utiliser une API pour une gestion de système de son. Cette API, qui m'est imposée, contient des descripteurs immutables. Tout passe par les constructeurs :

/** Descripteur du système sonore dans son ensemble */
public class SoundSystemDescriptor {

    public SoundSystemDescriptor(SoundDeviceDescriptor... soundDevices) {
        this.soundDevices = soundDevices;
    }

    // attributs, set, get ...
}

/** Descripteur d'un périphérique */
public class SoundDeviceDescriptor {

    public SoundDeviceDescriptor(String id, SoundChannelDescriptor... soundChannelDescriptors) {
        this.id = id;
        this.soundChannelDescriptors = soundChannelDescriptors;
    }

    // attributs, set, get ...
}

/** Descripteur d'un canal */
public class SoundChannelDescriptor {

    public SoundChannelDescriptor(String id, String name) {
        this.id = id;
        this.name = name;
    }

    // attributs, set, get ...
}

Maintenant, je dois implémenter la classe suivante me permettant de construire un SoundSystem à partir d'un SoundSystemDescriptor :

public class SoundSystemBuilder {
    public SoundSystem build(SoundSystemDescriptor soundSystem) {
        ...
    }
}

Premier test unitaire


Voici la première version de test unitaire que je pourrais écrire :

public class SoundSystemBuilderTest {

    private SoundSystemBuilder soundSystemBuilder;

    @Before
    public void setUp() throws Exception {
        soundSystemBuilder = new SoundSystemBuilder();
    }
 
    @Test
    public void testBuild() {
        SoundChannelDescriptor soundChannelDescriptor1 = new SoundChannelDescriptor("ch1", "ch1");
        SoundChannelDescriptor soundChannelDescriptor2 = new SoundChannelDescriptor("ch2", "ch2");
        SoundChannelDescriptor soundChannelDescriptor3 = new SoundChannelDescriptor("ch3", "ch3");
        SoundDeviceDescriptor soundDeviceDescriptor1 = new SoundDeviceDescriptor("d1", soundChannelDescriptor1 , soundChannelDescriptor2);
        SoundDeviceDescriptor soundDeviceDescriptor2 = new SoundDeviceDescriptor("d2", soundChannelDescriptor3);
        SoundSystemDescriptor soundSystemDescriptor = new SoundSystemDescriptor(soundDeviceDescriptor1, soundDeviceDescriptor2);
        SoundSystem soundSystem = soundSystemBuilder.build(soundSystemDescriptor);
        Assert.assertNotNull(soundSystem);
        Assert.assert...(...);
        Assert.assert...(...);
        ...
    }
}

Ce code est correct mais pas très lisible : le code de préparation est long, verbeux et pas très intéressant, la ligne correspondant au test est noyée dans la masse, etc ...

Premières améliorations


Mon premier réflexe dans une telle situation est de mettre toutes ces créations de descriptors dans des méthodes, elles-même regroupées dans une Factory. En plus, par expérience, je sais qu'une telle Factory sera très vite réutilisée dans d'autres tests unitaires, mon produit étant bâti autour du SoundSystem.

Je ne donne pas de code en exemple pour ne pas surcharger l'article, mais pour avoir une factory réutilisable, on voit bien qu'il va falloir mettre plein de paramètres aux méthodes, avec des difficultés pour hiérarchiser les paramètres de Device et ceux des Channels contenus dans des Device etc ...

DSL à la rescousse


En fait, je pars du code que j'aimerai avoir au final, un code minimaliste, explicite et évolutif. Mon idée est de créer un SoundSystemDescriptor avec un minimum de code, par exemple comme ça :

@Test
public void testBuild() {
    SoundSystemDescriptor soundSystemDescriptor = 
         prepareSoundSystemDescriptor()
         .addDevice("d1", channel("ch1", "ch1"), channel("ch2", "ch2"))
         .addDevice("d2", channel("ch3", "ch3"))
         .build();
    SoundSystem soundSystem = soundSystemBuilder.build(soundSystemDescriptor);
    Assert.assertNotNull(soundSystem);
    Assert.assert...(...);
    Assert.assert...(...);
    ...
}

Ensuite, je complète le code pour faire comprendre à Eclipse ce que je veux faire, notamment en ajoutant SoundDescriptorsFactory aux bons endroits, et en les faisant à nouveau disparaitre ensuite grâce aux import static :

SoundSystemDescriptor soundSystemDescriptor = 
    SoundDescriptorsFactory.prepareSoundSystemDescriptor()
        .addDevice("d1", SoundDescriptorsFactory.channel("ch1", "ch1"), channel("ch2", "ch2"))
        .addDevice("d2", channel("ch3", "ch3"))
        .build();

Ensuite, je "suis" le compilateur en créant les méthodes nécessaires (je m'aide du CTRL+1 très utile), avec les paramètres souhaités.

Le principe consiste à créer un Helper dans la méthodes prepareSoundSystemDescriptor(), et de créer les "vrais" descripteurs dans la méthode build(). Voici le code final de SoundDescriptorsFactory :

public class SoundDescriptorsFactory {

    public static SoundSystemDescriptorHelper prepareSoundSystemDescriptor() {
        return new SoundSystemDescriptorHelper();
    }

    public static SoundChannelDescriptor channel(String id, String name) {
        return new SoundChannelDescriptor(id, name);
    }
 
    // Inner classes ---------------------------------------------
 
    public static class SoundSystemDescriptorHelper {

        private List<SoundDeviceDescriptor> soundDeviceDescriptors = new ArrayList<SoundDeviceDescriptor>();

        public SoundSystemDescriptorHelper addDevice(String id, SoundChannelDescriptor... soundChannelDescriptors) {
            soundDeviceDescriptors.add(new SoundDeviceDescriptor(id, soundChannelDescriptors));
            return this;
        }

        public SoundSystemDescriptor build() {
            return new SoundSystemDescriptor(soundDeviceDescriptors.toArray(new SoundDeviceDescriptor[soundDeviceDescriptors.size()]));
        }
    }
}

Conclusion


Le code de la classe SoundDescriptorsFactory n'est pas trop complexe, mais peut vite le devenir selon les besoins, avec notamment d'autres inner class Helper. Néanmoins, à peu de frais, on obtient un code de tests unitaires répondant aux objectifs : minimaliste, explicite et évolutif.

Aucun commentaire:

Enregistrer un commentaire