Tests d'intégration : détecter les problèmes lorsque les composants communiquent
Vous avez écrit une fonction qui semble correcte. Le test unitaire passe. La logique est propre. Puis vous déployez, et l'application commence à retourner des erreurs. La colonne de base de données que vous pensiez exister a été renommée la semaine dernière. L'API externe que vous appelez a changé le format de sa réponse. Le service dont vous dépendez attend désormais un en-tête différent.
C'est le vide que les tests unitaires ne peuvent pas combler. Une fonction peut être parfaitement correcte isolément et pourtant échouer dès qu'elle tente de communiquer avec un autre composant. Les tests d'intégration existent précisément pour détecter ce genre de problèmes.
Ce que les tests d'intégration vérifient réellement
Les tests d'intégration vérifient que deux composants ou plus fonctionnent correctement ensemble. Ces composants peuvent être votre application et une base de données, votre service et une API externe, ou deux services internes au sein du même système.
Les bugs qu'ils détectent concernent rarement une logique erronée. Ils portent sur des hypothèses incompatibles :
Le diagramme de séquence suivant illustre une incompatibilité typique : un service envoie une date sous forme de chaîne, mais la base de données attend un timestamp, ce qui provoque une erreur.
- Votre code envoie une date sous forme de chaîne, mais la colonne de la base de données attend un timestamp.
- Votre service appelle une API avec un paramètre de requête, mais l'API a déplacé ce paramètre dans le corps de la requête.
- Votre application suppose qu'un champ est toujours présent, mais le service amont ne l'inclut que sous certaines conditions.
Ce ne sont pas des bugs que vous pouvez trouver en lisant le code. Ils n'apparaissent que lorsque les composants échangent réellement des données.
Le piège de la fragilité
Les tests d'intégration ont la réputation d'être lents et fragiles. Cette réputation est méritée. Plus vous impliquez de composants réels, plus vos tests risquent d'échouer pour des raisons indépendantes de votre code : un timeout réseau, un service dépendant qui est down, des données de test corrompues par une exécution précédente.
Lorsque cela se produit de manière répétée, les équipes cessent de faire confiance aux résultats des tests. Elles commencent à sauter les tests d'intégration ou à ignorer les échecs. Les tests deviennent du bruit au lieu d'être un signal.
La solution n'est pas d'éviter les tests d'intégration. La solution est d'être sélectif sur ce que vous testez avec des dépendances réelles.
Choisir quoi tester avec des dépendances réelles
Toutes les dépendances n'ont pas besoin d'être réelles dans vos tests d'intégration. La règle empirique est simple : testez avec une dépendance réelle uniquement pour les choses difficiles à simuler ou qui causent fréquemment des problèmes en production.
Les bases de données valent généralement la peine d'être testées avec une instance réelle. Le comportement des requêtes, les contraintes, les transactions et le verrouillage sont difficiles à simuler avec précision. Un mock peut vous dire que votre requête est syntaxiquement correcte, mais il ne vous dira pas que votre requête provoque un interblocage en accès concurrent, ou que votre migration a changé un type de colonne que votre code traite encore comme une chaîne.
Les API externes tierces ne valent généralement pas la peine d'être testées avec des endpoints réels dans votre pipeline. Utilisez des doubles de test qui enregistrent les réponses typiques. Le risque de tests instables dus à des problèmes réseau ou à des limites de débit de l'API l'emporte sur le bénéfice. Réservez l'intégration réelle pour la validation en préproduction ou en production.
Les services internes de votre organisation se situent entre les deux. Vous pouvez tester avec des instances réelles si l'interface change fréquemment et que le coût d'une incompatibilité est élevé. Sinon, les tests de contrat offrent souvent un meilleur signal avec moins de fragilité.
Un moyen pratique de décider : demandez-vous : « Si cette dépendance a un problème, le saurais-je par la logique applicative, ou seulement par la manière dont elle se connecte ? » Si la réponse est « par la manière dont elle se connecte » — format de réponse, structure d'en-tête, ordre des paramètres — alors c'est un candidat pour un test d'intégration avec une dépendance réelle. Si la réponse est « par la logique applicative », les tests unitaires ou les tests de contrat sont suffisants.
Maintenir les tests d'intégration rapides et fiables
Une fois que vous avez décidé quoi tester avec des dépendances réelles, suivez ces pratiques pour que vos tests d'intégration restent utiles :
Testez uniquement la connexion, pas la logique métier. Si vous avez déjà des tests unitaires couvrant vos règles métier, ne les répétez pas dans les tests d'intégration. Un test d'intégration pour une requête de base de données doit vérifier que la requête s'exécute avec succès contre une base de données réelle et retourne la structure attendue. Il ne doit pas vérifier tous les cas limites de la logique métier qui utilise cette requête.
Réinitialisez l'environnement avant chaque test. Si vous utilisez une base de données, créez des données de test isolées et nettoyez-les après la fin du test. Les tests qui dépendent de l'état laissé par les tests précédents sont fragiles et difficiles à déboguer. Utilisez des transactions de base de données qui rollback après chaque test, ou lancez un nouveau conteneur de test frais pour chaque exécution de test.
Limitez le nombre de tests d'intégration. Vous n'avez pas besoin de tester toutes les combinaisons de paramètres. Testez un chemin nominal et quelques scénarios d'échec réalistes. L'objectif est la confiance que la connexion fonctionne, pas la couverture de toutes les entrées possibles.
Où placer les tests d'intégration dans votre pipeline
Les tests d'intégration se situent entre les tests unitaires et les tests de bout en bout. Ils sont plus coûteux que les tests unitaires mais plus rapides et plus ciblés que les tests de bout en bout.
Un pipeline typique exécute d'abord les tests unitaires. S'ils passent, le pipeline exécute les tests d'intégration. Si les tests d'intégration passent, le pipeline procède au déploiement en préproduction ou en production. Les tests de bout en bout, si vous en avez, s'exécutent plus tard ou dans un environnement séparé.
Le but des tests d'intégration n'est pas d'atteindre un pourcentage de couverture. Le but est de vous donner la certitude que lorsque votre code change, les connexions entre les composants fonctionnent toujours.
Liste de contrôle pratique
- Pour chaque dépendance externe, décidez : test avec instance réelle, test avec double de test, ou recours aux tests de contrat.
- Exécutez les tests d'intégration dans un environnement isolé qui peut être réinitialisé à un état connu.
- Gardez les tests d'intégration concentrés sur le comportement de la connexion, pas sur la logique métier.
- Limitez les tests d'intégration à un chemin nominal et quelques scénarios d'échec réalistes.
- Surveillez le temps d'exécution des tests. Si les tests d'intégration prennent plus de temps que les tests unitaires, vous en avez probablement trop ou du mauvais type.
L'essentiel à retenir
Les tests d'intégration répondent à une question que les tests unitaires ne peuvent pas : « Ces composants fonctionnent-ils réellement ensemble ? » La réponse vaut la peine d'être connue avant de déployer. Mais les tests d'intégration sont un outil, pas un objectif. Soyez sélectif sur ce que vous testez avec des dépendances réelles, gardez les tests rapides et isolés, et utilisez-les pour renforcer la confiance dans vos déploiements, pas pour courir après des chiffres de couverture.