Puis-je avoir confiance en mes tests ?

Il existe un certain nombre d’outils permettant de fournir des métriques qui visent à mesurer la qualité d’un logiciel, notamment à travers l’analyse statique dont j’avais eu déjà l’occasion de parler. Ces métriques sont plus ou moins représentatives de la qualité du code. Mais nous permettent-elles de savoir si nous pouvons avoir confiance en notre code ?

Jusqu’à nouvel ordre, pour s’assurer que du code remplit correctement les besoins, nous n’avons pas vraiment d’autre solution que de le tester, et nous le faisons de manière aussi automatique que possible pour augmenter notre productivité. Mais nos tests sont-ils fiables ? Puis-je sortir une nouvelle version de mon produit en toute confiance ou dois-je croiser les doigts à chaque mise à jour ?

Il existe un certain nombre de façons permettant de savoir si nous pouvons avoir confiance en nos tests.

Retour d’expérience

Un bon indicateur pour savoir si nos tests sont de qualité est regarder dans le rétroviseur et de se poser les bonnes questions. Comment se sont passés les différents déploiements et mises à jour du logiciel ? Est-ce une opération totalement transparente ou, à l’inverse, une opération qui introduit de nombreux problèmes ?

D’ailleurs, à quelle fréquence livrez-vous des mises à jour ? Un cycle de release fréquent (ou encore mieux continu) est très certainement révélateur de la présence de tests efficaces et fiables. Si chaque mise à jour est laborieuse, la réaction typique est de les espacer pour ne pas être en crise perpétuelle. Malheureusement, dans ce cas de figure, les crises sont peut-être moins fréquentes mais bien souvent plus graves.

Quand vous livrez une nouvelle version, vous êtes plutôt à l’aise ou vous implorez le dieu du déploiement pour qu’il soit indulgent avec vous ?

Ah oui, j’oubliais, si vous n’avez pas de tests automatiques, ce n’est pas la peine de vous poser toutes ces questions, vous connaissez directement la réponse ! Je vous conseille dans ce cas-là de mettre en place des tests et ensuite de se poser toutes ces questions de manière itérative.

La réponse à ces questions donne une idée assez précise du degré de confiance que vous pouvez avoir en vos tests. C’est un simple constat, et à ce stade on ne sait pas forcément pourquoi on a plus ou moins confiance en ses tests, mais on sait déjà mesurer son degré de confiance.

Si vous évoluez avec une base de code très petite vous aurez certainement tendance à sous estimer le manque de confiance en vos tests parce que vous le compensez autrement. Vous arrivez encore à tester à peu près tout manuellement, vous vous forcez à le faire, et vous arrivez à trouver les bugs avant la production. Aussi, quand la quantité de code est réduite et le nombre de développeurs très restreint les risques d’introduire des régressions par effet de bord sont grandement limités. Mais cela ne veut pas dire que vous pouvez croire en vos tests. S’ils ne sont pas bien faits, il y a fort à parier que la fiabilité du logiciel va diminuer au fur et à mesure de sa croissance.

L’analyse de couverture

Les outils d’analyse de couverture de code permettent d’avoir une cartographie précise des lignes de code qui ont été parcourues lors de l’exécution des tests.  On peut facilement en faire un indicateur global de couverture de code sous forme de pourcentage de lignes de code (ou d’instructions).

Des outils existent dans la plupart des langages pour mesurer la couverture de code. Dans certains langages, c’est le cas en Java, ils sont directement intégrés aux IDE, ce qui facilite le lancement de l’analyse mais surtout la lecture de ses résultats puisque la couverture des lignes apparaît directement dans les fichiers source.

Cet indicateur est intéressant, mais il ne faut pas surinterpréter sa signification. Il prend en compte les instructions qui ont été parcourues pendant l’exécution des tests, mais ne nous garantit absolument pas que leur bon fonctionnement a été vérifié.

Intéressons-nous pour cela à deux exemples qui semblent très bêtes mais qui en fait se produisent plus souvent que ce que l’on croit. On ne s’en rend simplement pas compte parce qu’ils sont noyés au milieu d’une base de code plus grande.

public class IntegerAdder {
    public static int sum(int a, int b) {
        return a;
    }
}
public class IntegerAdderTest {
    @Test
    public void testSum() {
        int result = IntegerAdder.sum(8, 0); 
        assertThat(result).isEqualTo(8);
    }
}

Dans cet exemple, il y a un bug évident dans la méthode sum. Cette méthode est pourtant testée et l’unique test passe. Mais cet unique test couvre seulement un cas très particulier qui fait que le bug n’est pas mis en évidence. Dans cette situation, une analyse de couverture de code donne une couverture de 100%, et pourtant le code est de toute évidence incorrect.

public class Person {
    private final String firstName;
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

public class PersonReader {
    public Person readPerson() {
        Person person = new Person("Uncle", "Bib");
        System.out.println("Created person " + person.getFirstName() + " " + person.getLastName());
        return person;
    }
}
public class PersonReaderTest {
    @Test
    public void shouldReadUncleBob() {
        PersonReader personReader = new PersonReader();

        Person person = personReader.readPerson();

        assertThat(person.getFirstName()).isEqualTo("Uncle");
    }
}

Dans ce deuxième exemple, les vérifications faites dans le test ne sont pas complètes. Seul le champ firstName est vérifié, ce n’est pas le cas du champ lastName. Là encore nous avons une couverture de code de 100% et pourtant il y a un bug parce que nous nous attendrions à recevoir Uncle Bob et nous recevons Uncle Bib. Dans cet exemple, nous nous faisons tromper par la ligne qui écrit un message dans la sortie standard (une ligne de log tout à fait classique). Elle écrit le lastName de l’utilisateur, ce qui fait que la méthode Person.getLastName() est appelée. Elle est ainsi considérée comme couverte.

La couverture de code est un outil intéressant, mais attention à l’utiliser à bon escient et à ne pas lui faire dire ce qu’il n’a pas dit !

La mutation de code

Nous venons de voir que l’analyse de la couverture de code atteint vite ses limites lorsqu’il s’agit de mesurer la fiabilité des tests.

Mais alors, comment savoir si une instruction est vraiment testée ? Une des approches consiste à changer son comportement et de voir si au moins un test s’en aperçoit. Si aucun test n’est en échec, ce n’est pas bon signe…

Ce principe s’appelle la mutation de code. Il permet d’avoir des informations plus fiables que la couverture de code pour mesurer la qualité des tests, mais à condition qu’il soit appliqué sur de nombreuses instructions et que chaque instruction subisse plusieurs mutations. Il n’est absolument pas envisageable de mettre en œuvre manuellement ce genre de techniques tant la combinatoire est importante. Mais rassurez-vous, il existe comme bien souvent des outils qui s’occupent automatiquement de toute cette complexité.

C’est le cas de PIT en Java que j’ai découvert à Devoxx France 2014. Il s’intègre à votre système de build Maven ou Gradle et se charge d’exécuter vos tests en introduisant des mutations à l’intérieur de votre code. Vu que la combinatoire nombre d’instructions / nombre de mutations sur l’instruction devient vite énorme, l’exécution complète deviendrait vite très longue. Pour éviter d’exécuter tous les tests à chaque introduction d’un mutant, il analyse la couverture de code de chaque test de manière à savoir quels sont les tests susceptibles de tuer un mutant donné. Cela lui permet de n’exécuter que ces tests et accélérer l’analyse. L’exécution de PIT est tout de même relativement longue, et plus il y a de code plus c’est long.

PIT produit un rapport qui fait état du nombre de mutants qui ont été tués et également de ceux qui sont passés inaperçus. Chaque mutant passé inaperçu met en évidence une faiblesse dans les tests. L’instruction correspondante peut alors faire l’objet d’une régression potentielle sans qu’aucun test ne s’en rende compte.

Apprendre, encore et toujours

J’en avais déjà parlé, les outils d’analyse statique de code permettent de progresser en écriture de code en mettant en évidence des problèmes potentiels ou des mauvaises pratiques. De manière similaire, ce genre d’outils permet de progresser dans l’écriture de tests.

Le Test Driven Development est une technique de développement qui permet également de progresser dans l’écriture des tests. Elle consiste à écrire du code seulement pour satisfaire un test qui aura été écrit au préalable. Si l’on respecte à la lettre cette règle, toute instruction écrite dans le code est vérifiée par au moins un test, et on a alors une couverture de code de 100%. Très peu de mutants devraient survivre à une analyse de mutation. J’ai lancé à plusieurs reprises PIT sur du code écrit en TDD, et à chaque fois tous les mutants ont été tués. TDD est de toute évidence une technique qui permet d’écrire des tests fiables.

Avec un peu d’expérience en tests, même sans coder en TDD, on peut écrire des tests fiables. L’expérience aide également à évaluer la fiabilité d’un test donné. Ce genre d’outils permet alors de temps en temps de vérifier son intuition et l’ajuster si nécessaire.

Quelques conseils pour écrire de meilleurs tests

Le premier conseil que je peux donner et grâce auquel j’ai beaucoup appris c’est de se forcer à coder en TDD. Je ne dis pas que c’est une finalité en soi, mais ça force à faire autrement et c’est très enrichissant.

Vérifiez que vos tests sont exécutés en modifiant le code, ils devraient échouer. Si ce n’est pas le cas, posez-vous des questions !

Gardez à l’esprit que les tests sont ni plus ni moins une façon d’écrire les spécifications de votre code par l’exemple. Faites en sorte qu’ils soient assez exhaustifs. C’est exactement ce qu’il manque avec la méthode sum dans l’exemple que j’ai donné. Le TDD est une très bonne façon d’apprendre à être exhaustif parce qu’il exige que toute modification de code soit requise pour satisfaire un test.

Faites attention à vos assertions. Personnellement je recommande de ne pas vérifier les résultats champ par champ mais plutôt dans leur intégralité. J’écrirais ainsi :

assertThat(result).isEqualTo(new Person("Uncle", "Bob"));

Cela implique par contre en Java que les méthodes equals et hashcode de votre objet soient implémentées correctement. Lisez cet article au besoin pour automatiser leur génération et ainsi éviter des erreurs bêtes.

Conclusion

Si votre chaîne d’intégration continue comporte de nombreux tests et que, malgré le temps que vous avez passé à la mettre en place et la maintenir, vous n’êtes toujours pas confiant lors de vos livraisons, il est pertinent de se poser des questions parce que vous payez le coût de maintenir des tests sans bénéficier des avantages correspondants.

Le déploiement continu dans le monde du logiciel, c’est possible. C’est simplement une histoire de confiance dans ce qu’on fait. Et l’absence de confiance n’est pas une fatalité, un code testé correctement ne vous trahira pas souvent. Il est bien évident qu’on n’est jamais à l’abri d’un problème tant les systèmes que nous construisons sont complexes, mais ça doit rester de l’ordre de l’extraordinaire.  Mettre en place une batterie de tests qui permet de livrer en toute confiance a bien entendu un coût, mais pour l’avoir expérimenté, d’une part c’est possible et d’autre part on gagne énormément de temps par la suite. C’est un investissement !

Utilisez des outils pour mesurer et apprendre à mieux tester, mais méfiez-vous de l’interprétation que vous faites des métriques qu’ils vont vous donner !

Enfin, j’insiste au risque d’être lourd, forcez-vous à faire du TDD, j’ai personnellement énormément appris avec cette technique parce qu’elle impose par construction une grande méthode et rigueur pour écrire ses tests et son code. Une fois qu’elle est bien maîtrisée, il y a débat sur le fait de l’utiliser systématiquement, mais je crois que l’expérience permet d’avoir un jugement plus pragmatique au cas par cas pour savoir si elle est adaptée ou pas à ce qu’on fait.

L’image d’en-tête nous montre l’Airbus A350 pendant sa campagne d’essais en train de passer le test d’ingestion d’eau. Elle provient de la galerie d’images d’Airbus. Et oui dans tous les domaines de l’industrie on teste !

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *