02 mai 2014

To test or not to test ?

Pendant des années j'ai fait la promotion des tests unitaires, expérimenté tout un tas d'outils (c'est le côté rigolo) sans moi même trop me plier à la pratique. Je pouvais prétexter intervenir sur des couches techniques et/ou d'intégration qui sont d'un compliqué ma pauv' dame.

Bref faites ce que je dis, pas ce que je fais.

J'ai utilisé le TDD une fois lorsque j'ai voulu ré-implémenter fonzie proprement (la version initiale étant au mieux un jouet, ce qui n'a pas empêché de le mettre en production, m'enfin). Il faut reconnaître que ça marche du tonnerre si on arrive à se discipliner.

J'arrive après la bataille et je ne vais donc pas vous faire l'apologie des tests unitaires, d'autre l'ont fait et le font encore bien mieux que moi avec nettement plus de crédibilité. Ce que je constate par contre, c'est que c'est une pratique qui a un impact fort sur les choix d'architecture et de frameworks.

Architecture : une grosses appli Java EE de centaines de milliers de lignes, avec toute la bonne volonté du monde ça met vite un temps fou à builder/tester. Je pourrais vous parler de micro-services (un autre jour ?) mais il est évident qu'avec une suite de test qui prend plusieurs minutes et écroule la machine, le développeur ne va pas lancer les tests régulièrement, ni sans doute avant de committer des modifs mineures (sic!) sauf à faire la police dans l'open-space - bonne ambiance garantie.
Ca veut donc dire qu'il faut savoir cibler les tests à l'essentiels, le 100% de couverture qui teste tous les getter/setter (si si, y'en a) n'apporte vraiment que du surpoids au projet.

Frameworks : Je vous parlais hier de fluent-http, et sans surprise ce framework web permet d'écrire des tests très léger et très rapides. Il sait démarrer sur un port libre aléatoire (ce qui permet aux tests de tourner en parallèle), et ce en quelques centaines de millisecondes.

public class MyResourceTest {

    @BeforeClass
    public static void runWebServer() throws Exception {
        WebServer server = new WebServer(routes -> {
            routes.add(MyResource.class);
        }).startOnRandomPort();
    }


    @Test
    public void should_create_user() {
        given()
           .body("{ \"name\": \"nicolas\" }")
        .when()
           .post("/user")
        .then()
           .assertThat().statusCode(CREATED);
        // TODO test database insert was successful
    }
}


Ce tout petit test me permet de tester mon service REST, il manque juste un peu de mocking pour la partie backend. Il est de niveau API, donc orienté fonctionnel, et me permet de jouer avec l'implémentation.

Jouer c'est bien le mot, car avec des tests aussi rapides, on peut activer infinitest.

Infinitest joue les tests à chaque modification de code (compilation si vous utilisez IntelliJ Idea). On va donc tripatouiller le code par petites touches, Cmd+S et hop le test vérifie qu'on a pas tout cassé et/ou qu'on a enfin réussi à implémenter la fonctionnalité attendue.

On est donc dans une logique de type micro-intégration-continue, très très efficace. Evidemment, si vous n'avez pas des tests rapides, ça n'est pas d'un grand intérêt. C'est donc un ensemble complet de pratique, d'outils, et d'architecture auquel il faut s'attaquer, tous d'un même front.

Pour en arriver là sur des projets non triviaux, il faut aussi savoir découper son appli correctement. Mais je vous parlerais de micro-services une autre fois :)