Pourquoi les tests unitaires doivent être placés en tête de votre pipeline
Imaginez que vous poussiez une modification de code un vendredi après-midi. La build passe, le déploiement s'effectue, et vous rentrez chez vous. Samedi matin, votre téléphone s'allume avec des alertes. Un calcul de réduction applique des valeurs négatives aux commandes clients. La logique semblait correcte lors de la revue. Mais personne n'a détecté le cas limite où un code promo combiné à un prix soldé produit un total négatif.
C'est le genre de problème que les tests unitaires sont conçus pour détecter. Non pas parce qu'ils sont sophistiqués, mais parce qu'ils s'exécutent rapidement, tôt dans le processus, et de manière isolée. Ils constituent la première ligne de défense dans tout pipeline de livraison.
Ce que les tests unitaires vérifient réellement
Un test unitaire vérifie un comportement métier significatif à partir du point d'entrée où ce comportement commence. Dans un service backend, ce point d'entrée peut être un endpoint REST ou un cas d'utilisation. La requête est autorisée à traverser les couches internes réelles : contrôleur, service, logique métier et frontière du repository. Le test ne cherche pas à prouver qu'une méthode en appelle une autre. Il cherche à prouver que le système répond correctement à une entrée significative.
Si vous avez une fonction qui calcule les frais de port en fonction du poids et de la destination, un test unitaire peut confirmer :
- Un poids standard donne un coût standard
- Un poids nul donne un coût nul
- Un poids négatif renvoie une erreur ou zéro
- Le poids maximum renvoie la valeur plafond
Ce que le test unitaire ne doit pas chercher à prouver, c'est si la base de données réelle stocke correctement ce coût, si la passerelle de paiement réelle l'accepte, ou si un service voisin est réellement disponible. Ce sont des préoccupations pour d'autres types de tests.
La valeur des tests unitaires est étroite mais profonde. Ils vous donnent la certitude qu'un comportement spécifique fonctionne toujours lorsque le système environnant est contrôlé. Lorsque vous modifiez du code plus tard, des tests unitaires qui passent vous indiquent que vous n'avez pas cassé le comportement déjà vérifié.
Le principe d'isolation
Pour que les tests unitaires soient rapides et fiables, le monde extérieur doit être contrôlé. Les couches applicatives internes doivent s'exécuter normalement. Les voisins externes ne doivent pas décider du résultat du test. Cela signifie généralement pas de connexion réelle à la base de production, pas d'appel HTTP direct à un tiers, et pas de dépendance à un autre service en cours d'exécution. Si le comportement nécessite des données, le test peut utiliser des données contrôlées, une base de test en mémoire/locale, ou un faux, un mock ou un stub à la frontière du système.
C'est pourquoi le test unitaire ne doit pas être défini comme "un test par méthode" ou "un test par classe". Cette définition est trop mécanique et pousse souvent les équipes à tester des détails d'implémentation. Un test unitaire est mieux compris comme un test de comportement à partir d'un point d'entrée pertinent, avec le monde extérieur suffisamment contrôlé pour que l'échec renvoie au comportement testé.
Le code mobile est un exemple utile. Certains comportements n'ont de sens qu'au sein d'un runtime mobile. Dans ce cas, utiliser un émulateur ou un simulateur ne rend pas automatiquement le test incorrect. La question est de savoir si le test se concentre toujours sur un seul comportement et contrôle les dépendances autour de lui. Si c'est le cas, il peut toujours remplir le rôle d'un test unitaire dans le pipeline.
C'est cette isolation qui rend les tests unitaires rapides. Une suite de tests unitaires bien écrite pour un service backend typique se termine en secondes, pas en minutes. Comparez cela aux tests d'intégration qui lancent des conteneurs ou se connectent à des bases de test. Ceux-ci prennent des minutes.
La vitesse compte car les tests rapides sont exécutés plus souvent. Les développeurs les exécutent localement avant de pousser le code. Les pipelines CI les exécutent immédiatement après la build. Si quelque chose casse, vous le savez en quelques secondes ou minutes, pas après avoir attendu une suite de tests complète qui prend une demi-heure.
Où placer les tests unitaires dans le pipeline
Les tests unitaires doivent être placés au stade le plus précoce de votre pipeline, juste après la compilation ou la build du code. La logique est simple : si le comportement de base exposé par l'application est déjà cassé, il est inutile d'exécuter des tests plus lents qui dépendent de systèmes voisins réels.
Un ordre typique des étapes du pipeline ressemble à ceci :
Voici un exemple pratique de l'apparence de cette première étape dans un fichier de configuration CI :
# .gitlab-ci.yml ou configuration CI similaire
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
test-unit:
stage: test
script:
- npm test -- --coverage
only:
- merge_requests
- main
- Build ou compilation du code
- Exécution des tests unitaires
- Analyse statique ou linting
- Construction des images conteneur ou des artefacts
- Exécution des tests d'intégration
- Déploiement en staging
- Tests de bout en bout ou d'acceptation
- Déploiement en production
Si les tests unitaires échouent à l'étape 2, le pipeline s'arrête. Aucun conteneur n'est construit. Aucun environnement de staging n'est occupé. Aucun temps n'est perdu à attendre des tests d'intégration qui échoueraient de toute façon parce que la logique sous-jacente est erronée.
C'est la boucle de feedback rapide. Plus tôt vous détectez un bug, moins il coûte cher à corriger. Un bug trouvé lors des tests unitaires coûte quelques minutes à corriger. Un bug trouvé en production coûte une réponse à incident, des procédures de rollback, une communication client, et éventuellement une réparation de données.
Quand les tests unitaires ne suffisent pas
Les tests unitaires ont un angle mort : ils ne peuvent pas vérifier que les composants fonctionnent ensemble dans le système réel. Si votre comportement de checkout dépend d'une API de paiement externe, un test unitaire peut vérifier comment votre code se comporte lorsque la dépendance de paiement renvoie un succès, un échec, un timeout ou des données malformées. Il ne peut pas vous dire si l'API de paiement réelle accepte votre format de requête, gère l'authentification ou renvoie la structure de réponse attendue.
Pour cela, vous avez besoin de tests d'intégration. Mais les tests unitaires ont toujours un rôle à jouer ici. Ils vérifient que la structure de votre code est correcte, que les paramètres sont passés dans le bon ordre et que la gestion des erreurs fonctionne comme prévu. Ils ne peuvent simplement pas remplacer la vérification d'intégration réelle.
Une autre limitation est que les tests unitaires ne peuvent pas détecter les problèmes de configuration, les différences d'environnement ou les problèmes d'infrastructure. Une fonction qui fonctionne parfaitement dans les tests unitaires peut échouer en production parce que la base de données de production a un paramètre de collation différent, ou parce qu'une variable d'environnement requise est manquante. Ces problèmes nécessitent des approches de test différentes.
Quelle est la quantité suffisante de tests unitaires
La réponse dépend du risque. Si une fonction implémente une logique métier critique où une erreur pourrait entraîner une perte financière, une corruption de données ou des problèmes de sécurité, vous voulez des tests unitaires approfondis couvrant les cas normaux, les cas limites, les cas d'erreur et les conditions aux limites.
Si une fonction se contente de transmettre des données d'un endroit à un autre sans transformation, un seul test unitaire confirmant que le passage fonctionne correctement peut être suffisant. Passer des heures à écrire des tests exhaustifs pour du code trivial n'est pas une bonne utilisation du temps.
Une approche pratique est le test basé sur le risque. Identifiez les parties de votre codebase qui présentent le risque le plus élevé en cas d'échec. Concentrez vos efforts de test unitaire là-dessus. Pour le code à faible risque, écrivez juste assez de tests pour détecter les erreurs évidentes.
Liste de contrôle pratique pour les tests unitaires dans votre pipeline
- Les tests unitaires s'exécutent avant tout test d'intégration ou de bout en bout
- Les tests unitaires se terminent en quelques minutes pour l'ensemble du codebase
- Les tests unitaires ne nécessitent pas de services externes, de bases de données ou d'accès réseau
- Chaque test unitaire vérifie un comportement métier significatif à partir d'un point d'entrée pertinent, pas une méthode ou une classe
- Les tests sont déterministes : la même entrée produit toujours le même résultat
- Les tests unitaires qui échouent arrêtent immédiatement le pipeline
- Les développeurs peuvent exécuter les mêmes tests localement avant de pousser
L'essentiel à retenir
Les tests unitaires ne visent pas à atteindre des chiffres de couverture parfaits. Ils visent à détecter la classe de bugs la plus courante le plus tôt possible, avec le moins de temps et de coût d'infrastructure. Placez-les en premier dans votre pipeline, gardez-les rapides, gardez-les isolés, et concentrez-les sur la logique qui compte le plus. Tout le reste se construit sur cette base.