1. On regarde non pas le nom, mais deux axes
Dans les conversations sur les tests, on se perd facilement dès le niveau des mots. Dans une équipe, « integration test » — c'est presque n'importe quel test avec Spring. Dans une autre — seulement un live-server via un vrai HTTP. C'est pourquoi il est plus utile de s'appuyer non pas sur le nom du fichier, mais sur deux axes pratiques : ce qui fonctionne vraiment dans le test et ce que ce test peut prouver du tout.
Le premier axe — le volume d'environnement réel. Est-ce que tu lances un seul objet Java en mémoire ? Est-ce que tu lèves la couche web ? Est-ce que tu lèves un Spring context complet ? Est-ce que tu utilises un vrai serveur HTTP ? Est-ce que tu ajoutes un vrai PostgreSQL dans un conteneur ? Plus tu intègres de monde réel dans le test, plus le coût du lancement est élevé et plus il y a d'endroits potentiels où le test peut se casser pour une raison que tu n'es pas en train d'étudier.
Le deuxième axe — la zone de preuve. Certains tests attrapent mieux une règle locale, d'autres — les contrats de la frontière HTTP, d'autres encore — le wiring entre couches, d'autres — les nuances d'un environnement réel. Si tu gardes ces deux axes en tête, ça devient immédiatement plus simple : tu choisis non pas un test « à la mode », mais la profondeur de vérification adéquate pour un risque spécifique.
2. Tests unitaires : la protection locale du sens la moins chère
Le niveau unit — c'est l'endroit où tu vérifies un bout de comportement sans Spring, sans base, sans réseau et sans infrastructure aléatoire autour. Un tel test est presque toujours rapide, précis et peu coûteux à diagnostiquer. S'il tombe, la cause se comprend généralement en quelques secondes, sans aller démêler la moitié de l'application.
Dans ContentHub, les cibles naturelles pour les vérifications unitaires — ce sont les objets policy, les petites règles de domaine, la génération de slug, les restrictions de transitions entre statuts, les transformations de données locales. Tout ce qui peut honnêtement se faire observable comme une logique Java ordinaire. Par exemple, la règle « approve n'est autorisé que depuis IN_REVIEW » vit parfaitement à ce niveau.
// Objet policy : encapsule la règle de domaine (logique locale sans Spring/BDD/HTTP)
class PublicationPolicy {
// On vérifie si on peut exécuter approve depuis le statut actuel de l'article
boolean canApprove(ArticleStatus status) {
// Seul état autorisé pour approve — IN_REVIEW
// Tout autre statut (par ex. DRAFT) doit retourner false
return status == ArticleStatus.IN_REVIEW;
}
}
Un tel code n'a pas besoin d'ApplicationContext, de MVC ni d'une vraie base pour détecter une régression. Si demain quelqu'un « pour accélérer le processus » autorise approve depuis DRAFT, le test unitaire doit l'attraper instantanément. Et c'est un argument fort en faveur du niveau unit : ici tu attrapes la panne là où elle est née, et non après une longue chaîne d'effets indirects.
Mais il est important d'être honnête jusqu'au bout. Le test unitaire ne prouve pas que l'endpoint HTTP appelle cette règle correctement. Ne prouve pas que les données ont été sauvegardées en base. Ne prouve pas que la sécurité ne laisse pas passer un utilisateur supplémentaire. Il protège le sens local. Et c'est précisément pourquoi il est si précieux : il ne promet pas plus que nécessaire.
3. Tests slice : infrastructure réelle d'une couche
Dès que le risque quitte le niveau « règle pure » pour le niveau HTTP, JSON ou persistence, l'approche unit commence soit à ne pas assez couvrir le problème, soit à dégénérer en théâtre de mocks. C'est précisément là que, dans l'écosystème Spring Boot, l'idée des test slices est particulièrement utile : tu lèves non pas toute l'application, mais une tranche contrôlée dans laquelle l'infrastructure d'une couche spécifique devient réelle.
Les tests slice ont une philosophie importante. Ce n'est pas une « sous-intégration » ni un compromis de pauvreté. C'est une façon normale de vérifier une couche honnêtement et rapidement. Pour la couche web, il est important que le routage, le binding des paramètres, la validation, la sérialisation et la traduction des erreurs fonctionnent vraiment. Pour les contrats JSON, c'est le comportement réel du sérialiseur qui compte. Pour la couche données, ce sont les requêtes, les mappings, les contraintes et le comportement général de cette couche. Dans tous ces cas, monter le backend complet n'est pas obligatoire — et souvent simplement préjudiciable.
C'est pourquoi le niveau slice dans ce cours sera une part énorme de la pratique quotidienne. Il enseigne une discipline très utile : rendre réel exactement le bout d'infrastructure où vit le risque, et ne rien traîner d'autre sans raison. C'est l'une des compétences les plus professionnelles dans un projet Spring Boot ordinaire.
4. Tests d'intégration : quand il faut vérifier la jonction, pas seulement la couche
Le test d'intégration commence là où la panne ne vit pas à l'intérieur d'une couche, mais à la connexion de plusieurs parties. Le contrôleur appelle le service, le service travaille avec le repository, à côté sont connectés les filtres de sécurité, le traitement des erreurs, la configuration, la sérialisation — et tu as besoin de voir comment tout cela se comporte ensemble. Ici, un test slice honnête ne suffit déjà plus, parce que le risque qui t'intéresse est né exactement à la jonction.
Dans les projets Spring Boot, le mot integration est souvent associé au contexte applicatif complet. C'est une association normale, mais ici l'important n'est pas le nom de l'annotation, mais l'idée : tu lances un morceau du système suffisamment large pour attraper le wiring, la configuration, la traduction des exceptions, la chaîne controller → service → repository et d'autres effets conjoints.
Le coût de ce niveau est aussi honnête. Il prend plus longtemps à démarrer, tombe de façon plus bruyante et localise moins bien la cause de l'échec. C'est pourquoi les tests d'intégration sont précieux non pas pour leur largeur en tant que telle, mais parce qu'ils couvrent exactement les risques qui vivent réellement entre les parties du système. Dès que tu oublies cette condition, commence l'abus du contexte complet.
5. Live-server : quand le vrai échange HTTP compte
Il y a encore une étape plus large : le test qui ne passe pas par un appel local à la couche web, mais par un vrai serveur HTTP levé. Du point de vue du développeur, c'est une frontière très importante. Le test web intra-processus et le live-server vérifient des scénarios similaires, mais pas la même réalité. Dans le second cas, l'application se comporte plus proche d'un vrai serveur, et le test communique avec lui comme un client externe.
Qu'est-ce que ça ajoute ? Primo, la confiance dans un vrai round-trip HTTP : le serveur est vraiment levé sur un port, la requête est vraiment partie et une réponse est revenue. Secundo, certains détails du comportement du transport, des filtres et de la configuration serveur deviennent mieux visibles. Tertio, ce mode montre très bien que le client « de l'extérieur » verra vraiment le système que tu promets, et non simplement un appel correct à l'intérieur de la JVM.
Mais le live-server ne peut pas être le mode par défaut pour tout le jeu de tests. Il est coûteux. Si tu vérifies une règle locale de workflow, le live-server est juste une décoration lente. Sa puissance se révèle uniquement là où la question se pose vraiment ainsi : « qu'est-ce qu'un vrai client HTTP verra ? »
6. Tests container : le comportement dépend de l'infrastructure
Les tests container ne sont pas là pour rendre tout « encore plus sérieux », mais pour arrêter de se mentir à soi-même sur le comportement d'un composant externe. Pour ContentHub, l'exemple principal sera un vrai PostgreSQL. Au niveau simplifié, les données peuvent sembler normales, mais sur une vraie base apparaissent soudainement le dialecte, les contraintes, les timestamps, les tris et les migrations qu'on ne peut honnêtement pas simuler ni dans sa tête ni dans un remplacement de test aléatoire.
C'est pourquoi le test container n'est pas le « dernier et meilleur niveau ». C'est un outil séparé pour une catégorie séparée de risque : quand le comportement d'une infrastructure spécifique t'importe. Dans un tel test, tu paies en temps de démarrage et en complexité d'environnement supplémentaire, mais tu obtiens en échange une image bien plus honnête là où l'environnement réel change vraiment le résultat.
C'est précisément pourquoi dans une stratégie mature, le container-path apparaît généralement plus tard et de façon ciblée. Non pas comme remplacement de tous les autres tests, mais comme amplificateur pour les scénarios particulièrement sensibles. C'est une idée importante pour tout le cours : plus cher ne signifie pas mieux. Plus cher signifie « vaut la peine d'être appliqué là où il y a une valeur ajoutée » 💡
7. Tests de régression
Il y a encore un mot qu'on aimerait bien mettre au même rang que unit et integration — regression. Mais le jeu de régression est mieux compris non pas comme une technologie séparée, mais comme une partie spécialement sélectionnée de tout le test suite. Son but n'est pas d'être le plus grand ou le plus lourd. Son but — donner le maximum de valeur pour un temps de lancement raisonnable.
Dans un projet backend normal, le regression suite est presque toujours mixte. Il contient les tests unitaires rapides et précieux, quelques vérifications slice clés, quelques scénarios d'intégration coûteux mais très importants. Parfois des vérifications live-server y entrent aussi, si le coût est justifié. Quant aux tests container, ils restent plus souvent dans un sous-ensemble plus tardif et plus étroit, parce qu'ils sont sensiblement plus coûteux dans l'utilisation quotidienne.
Il est utile de comprendre ça dès maintenant : un bon suite n'est pas un tas de tests tous identiquement larges. C'est une composition de différents niveaux, où chacun paie pour lui-même par une valeur claire. Et la logique de regression aide justement à maintenir cette composition dans un état sain.
8. Comparaison des niveaux : ce qu'ils prouvent réellement
Il est utile de voir tous les niveaux côte à côte au moins une fois non pas comme un ensemble de mots à la mode, mais comme une table de forces et de limites.
| Niveau | Ce qui fonctionne vraiment en général | Attrape le mieux | Ne prouve pas en lui-même | Coût |
|---|---|---|---|---|
| Unit | une classe, une règle ou un petit service | règles métier locales, transformations, invariants | contrat HTTP, wiring, vraie base, security-chain | faible |
| Slice | une couche et son infrastructure | contrats JSON, frontière web et comportement d'une couche données spécifique | la chaîne complète de l'application et les couches voisines | faible / moyen |
| Integration | plusieurs couches ou contexte complet | wiring, travail conjoint controller/service/repository, configuration | nuances de transport du live-server et comportement d'une vraie infrastructure externe si elle est substituée | moyen / élevé |
| Live-server | vrai serveur HTTP et appel client | comportement web end-to-end du point de vue du client | spécificités de la BDD externe ou de l'intégration si elles ne participent pas vraiment | élevé |
| Container | application plus vrai composant externe | comportement de PostgreSQL, migrations, contraintes d'infrastructure | tout le reste « automatiquement » ; ce n'est pas un remplacement universel de tout le jeu de tests | élevé+ |
Cette table est utile parce qu'elle supprime la magie superflue. Par exemple, le test d'intégration n'est pas « meilleur que le test unitaire en général ». Il prouve simplement autre chose. Et le test unitaire n'est pas « plus faible que le live-server par définition ». Il répond simplement à une autre question d'ingénierie.
9. Un scénario utilisateur n'est pas égal à un seul test 💥
C'est là qu'apparaît le moment clé de la journée. Prends le même flow « editor submit → admin approve → public read ». Si tu le regardes avec les yeux d'un utilisateur, c'est une seule histoire. Si tu le regardes avec les yeux d'un ingénieur qualité, c'est un ensemble de risques différents et donc un ensemble de preuves différentes.
La règle locale de transition de statut s'attrape le mieux par un test unitaire. Le bon 404 sur public read se vérifie plus honnêtement à la frontière web. Le filtre sur PUBLISHED vit dans la couche données. La liaison transversale controller → service → repository a du sens au niveau intégration. Le vrai échange HTTP — en live-server. La spécificité de PostgreSQL — au niveau container.
Dès que ça devient visible, la tentation de chercher un type de test « universel » disparaît. Et c'est exactement à ce moment-là que toute la typologie cesse de paraître bureaucratique. Elle commence à fonctionner comme une carte de coût et de confiance. C'est-à-dire exactement comme elle doit fonctionner dans un backend professionnel 🚀
10. Erreurs typiques dans les conversations sur les niveaux de tests 💉
Erreur n°1 : penser qu'un test plus large est automatiquement meilleur.
Un test large voit effectivement plus d'environnement, mais en paie le prix en vitesse et en qualité de diagnostic. Il n'est utile que là où cette largeur apporte une nouvelle information.
Erreur n°2 : nommer le niveau par la bibliothèque, et non par le monde réellement levé.
Le nom de fichier SomethingIntegrationTest ne prouve rien. Regardez l'environnement factuel : qu'est-ce qui était réel, et qu'est-ce qui ne l'était pas.
Erreur n°3 : attendre des tests unitaires des garanties sur le contrat HTTP ou le comportement de la base.
Ce n'est pas un défaut des tests unitaires, c'est une mauvaise attente. Ils sont bons précisément parce qu'ils attrapent les risques locaux rapidement et précisément.
Erreur n°4 : essayer de tout faire via live-server ou contexte complet.
Un tel suite devient rapidement coûteux, bruyant et peu maniable dans l'utilisation quotidienne. Si le développeur n'a pas envie de lancer les tests souvent, la stratégie a déjà perdu.
Erreur n°5 : percevoir le regression suite comme « le plus grand jeu de tests ».
Le jeu de régression est précieux non par sa taille, mais par son ROI. Sa tâche — donner rapidement et régulièrement un signal fort sur les changements, et non impressionner par la longueur du log.
GO TO FULL VERSION