Les stratégies de test

J'en peut plus des tests soit-disant unitaires ! Tout le monde fait des "tests unitaires" . C'est devenu un mot magique. Et ça ne veut plus rien dire. C'est plutôt l'enfer des tests automatiques : lenteurs, invasion de mocks, faux-positifs, découragement, dénigrement des tests unitaires, etc. Qui n'a pas vécu ça au boulot ? Regardons ça de plus près.

La jungle des tests automatiques

Les tests automatiques ont des tas de variantes : test d'acceptation, test bout-en-bout (end to end), behaviour test, test fonctionnels, test de régression, test de performance, test d'intégration, smoke test, pen test, test de résilience, fuzzy test, etc. chaque année a sa nouvelle variante de test automatique. Un test unitaire peut être un test de régression. Mais un test d'intégration aussi. Parcontre, un test d'intégration ne peut pas être un test unitaire. Pas facile de s'y retrouver.

Pour qu'un test automatique soit unitaire, il ne doit ouvrir aucun fichier, n'interroger aucun serveur, n'exécuter aucune commande système, n'afficher aucune fenêtre, etc. En un mot, le test unitaire ne fait aucune entrée/sortie. Pas même un gettimeofday! C'est pour ça qu'un test unitaire est extrêment rapide, on parle de quelques micro secondes au plus. Idéalement, votre test unitaire n'importe qu'une classe ou qu'une fonction.

Déjà, si on peut arrêter de parler de test unitaire pour tout ce qui ne rentre pas dans cette catégorie, ça sera ça de gagné ! Le test unitaire est un outils parmi d'autres pour votre stratégie globale de qualité. Si vos tests sont en souffrance, c'est le moment de prendre du recul.

Quelle stratégie adopter ?

D'abord, testez manuellement! Pour garantir le fonctionnement d'un logiciel, il faut l'utiliser. Désolé pour l'évidence. Cela permet en plus d'évaluer l'ergonomie et la possibilité d'automatiser.

Car bien sûr, on veut vite automatiser l'exécution des tests pour gagner du temps et éviter les regressions. Au fond, tout test automatique est un test de régression. Automatiser et mettre en CI des tests, c'est aussi garantir que le logiciel fonctionne effectivement hors du poste de développement.

Comme nous sommes partis de tests manuels, les premiers tests automatiques valident les fonctionnalités du logiciel d'un point de vue utilisateur. Ce sont les tests fonctionnels. Dans le cadre d'un test fonctionnel, mieux vaut être au plus près de l'environnement de prod : installation selon le manuel, base de donnée, reverse proxy, navigateur web, etc. On entre donc dans la catégorie des tests d'intégration : est-ce que tout fonctionne bien ensemble.

Oh là là, mais on a toujours pas parlé de TDD ni de tests unitaires !!!

Unitaire

Le test unitaire ne suffit pas à garantir le fonctionnement d'un logiciel. Il permet seulement de valider rapidement différentes combinaisons de paramètres d'une fonction et d'assurer le résultat ou la gestion d'erreur. Un bon exemple est l'écriture d'expression rationnelle. En devenant complexe, cela devient difficile de garantir tout les cas. Un test unitaire permet de rédiger tout ces cas et de les valider en moins d'un instant.

Un code testable unitairement est un critère de qualité de conception. D'où l'intérêt de rédiger les TU au plus tôt voire avant le code. La testabilité va guider la conception de vos classes et de vos fonctions vers plus de maintenabilité.

Pour les tests fonctionnels, concentrez-vous sur la validation des fonctions essentielles du logiciel plutôt que sur l'exhaustivité des erreurs à gérer. Reproduire un timeout dans des tests d'intégration est hasardeux. Utilisez les TU pour valider la gestion d'erreur. Vous gagnerez en temps de test et en simplicité.

L'enfer, c'est vouloir des tests fonctionnels sur son poste (mais sans installer toute l'infra), couvrant chaque if, chaque else et chaque except. On finit avec des mocks de base de données, d'API REST, etc. à ne plus savoir qu'en faire. Il y a plus de code de fixture que de test. On exécute 3000 fois certaines parties du code pour tester une fois le reste. La tâche de CI s'allonge...

J'attends la CI

Le mieux est parfois l'ennemi du bien

La banqueroute de la CI arrive lorsqu'on ne peut plus valider son travail. On lance les tests avant de jouer aux fléchettes, de rentrer à la maison. Ou encore on n'exécute plus la batterie complète des tests que la nuit, le week-end, etc. Dites adieu au déploiement continue voire simplement à finir un ticket dans une itération !

Pour remettre en cause ça : repartir de la liste des scénarios fonctionnels. Tous les tests qui ne sont pas fonctionnels peuvent partir à la benne. Chasser les tests redondants. Si vous avez une matrice complexes de combinaisons à évaluer, évincez judicieusement certaines combinaisons qui ont peu ou pas de chance de planter si les autres fonctionnent.

Enfin et surtout, distinguez test fonctionnel et unitaire. Pour un test fonctionnel supprimé, ajoutez quelques tests unitaires. En combinant les couvertures du code, vous pouvez vous assurez de ne pas régresser sur ce point.

Une dernière piste pour réduire le temps de test : exécutez les tests fonctionnels sur un environnement persistent. Cela raccourcis considérablement la création d'un environnement jetable. Rien ne vous empêche de recréez cet environnement chaque nuit ou chaque samedi. Cela nous amène à la dernière catégorie de test pour une stratégie complète.

Le testenprod®

À la grande époque, le passage en prod était quelque chose ! C'était le test ultime. Les vrais ne faisait que du test en prod selon la vieille maxime : tester, c'est douter. Parfois, on voyait passer un commit en urgence signé root@localhost... indiquant que le hostname n'est même pas configuré en prod. La culture de la CI a un peu réduit cela. On a un peu jeté le bébé avec l'eau de bain. Le test en prod est négligé, il faut retrouver sa valeur.

Dans certaines industries, le produit est livré avec une procédure de test intégrée, permettant de valider à tout moment le fonctionnement, à blanc, du produit fini. C'est le cas notamment dans l'embarqué. C'est intéressant aussi pour un logiciel.

Prenons l'exemple d'une boutique en ligne. Suis-je capable de vérifier régulièrement que ma boutique fonctionne, réellement, en faisant une commande à blanc ? Par exemple, avec un code promo particulier, la commande n'est ni livrée, ni facturée. Une fois que j'ai cette procédure, je peux l'exécuter régulièrement, mesurer le temps écoulé, et avoir un retour réel sur la santé de ma prod.

Conclusion

Voici un tableau récapitulatif des critères pour distinguer un test fonctionnel d'un test unitaire.

Test unitaire Test fonctionnel Test en prod
Vitesse Très rapide Assez lent Moyenne
Échelle de temps Quelques secondes Plusieurs minutes Une minute
Couverture du code Oui Non Non
Entrée/Sortie Aucune Oui Oui
Base de données Non Oui Oui
Langage d'écriture Python (comme le projet) Peu importe Peu importe
Exécution en CI Pour chaque commit Au moins sur master Non, en supervision.

Et vous ? Quelles sont vos bonnes habitudes des tests ?