Écrire des tests automatiques est une activité chronophage. Déjà, l'écriture en elle-même demande du temps. Mais ce qui en prend le plus, c’est la maintenance du test.
De plus, comme pour n’importe quel code, il est sujet à la dette technique. Il est donc utile de l’améliorer régulièrement, de le refactorer, de le nettoyer, etc.
À un moment ou un autre, il arrivera aussi que le test ne passe plus. Dans ce cas, il va falloir investiguer et mener l’enquête afin de savoir si le problème vient du code ou du test. Autrement dit, il faudra déterminer s'il y a vraiment une erreur ou si le test n'est plus à jour.
Enfin, le code qui est testé a de grandes chances d’évoluer.
Donc il va falloir encore passer du temps à corriger le test pour qu’il reflète les évolutions.
Vous l’avez compris : tester, c’est très bien et cela permet de gagner un temps précieux. Mais cette activité nécessite également d’y consacrer régulièrement du temps, comme pour n’importe quel autre code qui compose votre application.
Il faut donc réussir à être pragmatique et à essayer d’écrire le moins de tests possible tout en s'assurant qu’ils sont efficaces et utiles.
Passons à quelques astuces vous permettant de savoir ce qui donne de la valeur à un test. 😀
Des tests qui ne servent à rien
Ah ça, j’en ai vu dans ma carrière. 😜 Des batteries de tests écrits par des développeurs zélés, désireux de tester exhaustivement tout ce qui a été développé.
C’est très bien ; encore faut-il que les tests servent à quelque chose !
Prenons ce code, par exemple :
public int Additionne(int a, int b)
{
return a + b;
}
Pensez-vous qu’il y ait un intérêt à tester :
Additionne(4, 7);
Additionne(10, 20);
Et à vérifier que cela fait respectivement 11 et 30 ?
Non, absolument aucun intérêt ! L’addition, dans un langage de développement, cela fonctionne. C’est garanti, sinon ce n’est même pas la peine d’aller plus loin. 🙂
Avoir des tests inutiles est assez risqué.
En effet, ils vous donnent une fausse impression de sécurité. Vous avez énormément de tests, alors vous vous croyez très bien protégé, alors qu'en fait, vous ne l’êtes pas du tout !
C’est comme sauter à l'élastique avec un harnais qui n’est pas accroché au pont… 😱
Et si cela ne suffit pas, il y a encore pire ! Vous avez produit beaucoup de code qu’il va falloir faire évoluer, maintenir, remanier, nettoyer... Bref, beaucoup de temps perdu en perspective, mais aucun bénéfice à l’horizon.
Tester le code le plus important en premier
C'est logique ! Mais force est de constater que ce n’est pas toujours le cas. 😛
Il faut tester le code qui est le plus important en premier. Tester le code qui fait la force de votre application, de votre entreprise. C’est en général le code métier, celui pour lequel toute l’application a été écrite.
Vous avez un site d’e-commerce ? Il faut vérifier qu’un client puisse bien ajouter un produit à son panier et le payer. Pouvoir changer la date d’anniversaire de son chien est sans doute une fonctionnalité intéressante offerte par votre site, mais elle peut être testée après le plus important. 😉
Les caractéristiques d’un bon test
1. Un test doit avoir une forte probabilité de montrer des régressions
Cela se traduit notamment par le fait de tester un nombre important de lignes et de fonctionnalités. Mais pas seulement ! Ce n’est pas toujours qu’une question de lignes, mais aussi de signification. Tester du code métier, c’est bien, mais tester du code trivial n'a pas d'intérêt.
Rappelez-vous la méthode qui fait l’addition de deux entiers :
public static int Addition(int a, int b)
{
return a + b;
}
Vérifier que le résultat est 4, si l'on appelle cette méthode avec les paramètres 1 et 3, a très peu d’intérêt. Nous n’avons pas besoin de vérifier que l’opérateur +
fonctionne, car il fonctionne. En testant du code de ce genre, nous n’avons aucune chance de trouver des régressions.
2. Un test doit avoir une probabilité faible de montrer des faux positifs
Un faux positif correspond à un test qui échoue alors que la fonctionnalité testée est opérationnelle. Le test ne devrait pas échouer, il s’agit d’une fausse alarme.
Un faux positif peut arriver à plusieurs occasions, par exemple lors d’une phase de refactoring, lorsque le test ne correspond plus au code que l’on a créé.
Si un test échoue pour de mauvaises raisons, alors on risque de ne plus prêter attention au résultat du test et de l’ignorer. La conséquence est grave, car on risque de laisser passer une régression bien réelle.
Prenons l'exemple d'un test qui vérifie la météo. Lorsque l’on crée le test, il fait beau, donc on va vérifier qu’il y a du soleil. Par contre, le test n’est plus bon quand il pleut alors que la méthode testée est sans doute fonctionnelle. Le test est OK les jours de beau temps, mais KO les jours de mauvais temps. Ainsi, lorsque quelqu’un va développer une nouvelle fonctionnalité, puis lancer les tests, ce test ne passera pas. Le développeur ne sera pas en mesure de savoir si c’est parce que son travail a eu un impact sur l'application ou simplement si c'est parce qu’il ne fait pas beau aujourd’hui !
Un faux positif a plus de chance d’arriver lorsque le test est directement lié aux détails d’implémentation. Pour réduire les risques, il faut découpler le test de l’implémentation du système à tester. Vous pouvez traiter votre code comme une boîte noire, par exemple, comme si vous ne saviez pas ce qu'il y a à l'intérieur. Vous utilisez les entrées et vérifiez les sorties, sans chercher forcément à vérifier toutes les étapes possibles.
Pour illustrer ce problème, regardons l’exemple suivant :
static void Main(string[] args)
{
string chaine = "Une chaine à crypter";
string resultat = Crypte(chaine);
}
private static string Crypte(string phrase)
{
string etape1 = Etape1(phrase);
string etape2 = Etape2(etape1);
string etape3 = Etape3(etape2);
return etape3;
}
private static string Etape1(string phrase)
{
// peu importe l'algorithme de cryptage
}
private static string Etape2(string phrase)
{
// peu importe l'algorithme de cryptage
}
private static string Etape3(string phrase)
{
// peu importe l'algorithme de cryptage
}
Le but est de crypter une phrase en passant par plusieurs étapes de cryptage.
Un mauvais test consisterait à tester le résultat de chaque étape et à vérifier qu’il correspond bien à ce que l’on a développé. En effet, peut-être que notre algorithme en 3 étapes pourrait s’écrire en 1 seule étape après un refactoring particulièrement astucieux. Dans ce cas, notre test "étape par étape" va échouer, voire ne plus compiler. Le test produira donc un faux positif et du travail de maintenance. Au contraire, si nous testons simplement le résultat du cryptage, peu importe la façon dont est développé l’algorithme (en une ou plusieurs étapes), le résultat ne changera pas.
3. Un test doit apporter un feedback rapide
C’est très important, car plus on a un retour rapide, moins on perd de temps à partir dans la mauvaise direction.
Des tests très complets (de bout en bout, voire d’interfaces) sont particulièrement intéressants pour capturer les régressions sans faux positif. Malheureusement, ils sont en général très longs à exécuter, ce qui est un problème pour avoir un feedback rapide.
4. Un test doit avoir un coût de maintenance réduit
En général, c’est fortement lié à la taille du test. Plus il y a de lignes de codes dans un test, plus la maintenance sera compliquée et prendra du temps.
Écrivez des tests simples, qui ne testent pas énormément de choses. Il vaut mieux écrire 5 petits tests qu’un gros test qui teste 5 choses. Cela sera plus simple de déterminer celui qui échoue et simplifiera le code global.
Si le test porte sur des détails d’implémentation, il y aura non seulement un risque plus élevé de montrer des faux positifs, mais il sera beaucoup plus fragile au refactoring. Si je teste trois méthodes et qu’après un refactoring, il ne me reste qu’une seule méthode, alors il va falloir que je modifie les tests, ou que je les réécrive complètement.
Vous savez maintenant ce qui donne de la valeur à un test. Il faut arriver à trouver un bon ratio entre ces éléments, ils ne peuvent pas être tous atteints en même temps. Mais ne vous inquiétez pas, il est possible de s’en approcher. 😁
Résumé de la partie
Faire un test, c'est vérifier qu'une fonctionnalité de son application est opérationnelle.
On peut faire deux grands types de tests, manuels ou automatiques.
Les tests automatiques vont nous permettre de gagner du temps et d'éviter les erreurs humaines.
Nous allons apprendre à réaliser des tests unitaires et des tests d'intégration, car ce sont ceux qui ont le meilleur rapport utilité/coût.
Le TDD est une approche du développement qui propose d'écrire un test automatique validant une fonctionnalité, avant de la développer.
Les dépendances complexes à maîtriser devront être simulées lors de nos tests automatiques, grâce à des doublures de tests.