Où tracer la frontière entre tests unitaires et tests d'intégration
· Mis à jour le: · Go Komura · Tests, Tests unitaires, Tests d'intégration, Conception de tests, Développement Windows, C# / .NET
Dans les discussions de conception de tests, la question discrètement difficile qui revient à chaque fois est de savoir jusqu’où pousser les choses dans les tests unitaires, et à partir d’où les faire monter vers les tests d’intégration.
Ce qui est dangereux ici, ce sont les deux extrêmes suivants :
- « On veut que ça tourne vite, donc tout devient test unitaire »
- « C’est plus proche du réel, donc tout devient test d’intégration »
Le premier finit noyé sous les mocks et laisse facilement passer les points qui cassent en production ; le second tend à produire un ensemble de tests lent et fragile. En pratique, les axes à regarder sont un peu plus clairs :
- Ce que l’on veut vérifier, est-ce notre propre logique ou la connexion vers l’extérieur ?
- Si l’on remplace par un fake en mémoire, le sens se perd-il ?
- Le comportement de la base de données / des fichiers / de HTTP / du DI / de la configuration / du framework / de l’OS est-il le vrai sujet ?
- Veut-on faire tourner rapidement un grand nombre de motifs d’entrée ?
Une fois ces quatre points visibles, la frontière entre tests unitaires et tests d’intégration devient beaucoup plus facile à tracer.
Dans cet article, nous organisons la frontière entre tests unitaires et tests d’intégration de façon pratique, en nous appuyant sur les informations publiques de Microsoft Learn et de Martin Fowler disponibles en mars 2026.123
1. La conclusion d’abord
Dit de façon assez brute, mais sous une forme facile à utiliser en pratique, cela donne :
- La logique pure va dans les tests unitaires
- Les connexions, le câblage, les conversions et les différences d’environnement vont dans les tests d’intégration
- Si les deux permettent de vérifier, commencez par un test unitaire
- Plutôt que d’élargir et d’alourdir les tests d’intégration, resserrez-les sur la frontière
En un mot, le test unitaire est un « test de décision », le test d’intégration un « test de connexion ».
Les éléments dont le sens est complet sans ressource externe — calcul de montants, transitions d’état, validation des entrées, conditions d’approbation, classification des exceptions — gagnent, une fois poussés vers les tests unitaires, en rapidité, en robustesse, et permettent de couvrir plus densément les motifs d’entrée. À l’inverse, les éléments qui « vous trahissent dès qu’ils sont connectés » — exécution SQL, sérialisation JSON / CSV, routage, model binding, enregistrement DI, verrouillage de fichiers, autorisations, enregistrement COM, 32 bits / 64 bits, STA / MTA — sont plus sûrs du côté des tests d’intégration.
L’article Integration tests in ASP.NET Core de Microsoft Learn recommande de la même façon de resserrer les tests d’intégration sur les scénarios d’infrastructure importants, et de choisir un test unitaire chaque fois qu’il suffit.
2. Ce que cet article entend par test unitaire et test d’intégration
Voici comment nous distinguons ces termes ici.
| Niveau | Ce qu’il vérifie | Composition typique |
|---|---|---|
| Test unitaire | La justesse d’une responsabilité isolée | Utilise des fakes / mocks / stubs et coupe les ressources externes |
| Test d’intégration | La connexion entre plusieurs composants, et le comportement incluant l’infrastructure et les frameworks | Vraie base de données, vrais fichiers, vrai serializer, vrai host, vrai pipeline, etc. |
| Test E2E / fonctionnel | Le flux utilisateur à travers l’application entière | Application déployée, plusieurs services, vrai navigateur ou vrai processus |
Dans les recommandations .NET sur les tests unitaires, un bon test unitaire est décrit comme rapide / isolé / reproductible (fast / isolated / repeatable), sans dépendance à des facteurs externes comme le système de fichiers ou une base de données. Unit testing best practices for .NET explique cela clairement.
Par ailleurs, un test d’intégration ne désigne pas uniquement « un test lourd qui utilise forcément un autre processus ou un autre serveur ». Même au sein d’un seul et même processus, dès lors qu’on relie plusieurs composants réels et qu’on vérifie le comportement authentique du framework ou de l’infrastructure, cela penche du côté du test d’intégration.
Par exemple, pour tester en unitaire une action de controller ASP.NET Core, la documentation officielle indique de resserrer le sujet sur les décisions du corps de l’action, et de traiter les interactions côté framework comme routing, model binding ou filters dans des tests d’intégration. Unit test controller logic in ASP.NET Core présente cette répartition de façon claire.
3. Le tableau de décision en un coup d’œil
Voici d’abord le tableau le plus utile en pratique.
| Ce que l’on veut vérifier | Test principal | Remarque |
|---|---|---|
| Calcul de montant, remises, transitions d’état, validation des entrées | Test unitaire | On veut faire tourner densément les motifs d’entrée |
| Classification des exceptions, choix du message d’erreur, décision de réessayer | Test unitaire | Le sens est complet sans E/S réelles |
| Traduction SQL / ORM du Repository, transactions | Test d’intégration | Le vrai sujet est le comportement de la vraie base de données ou du vrai provider |
| Sérialisation / désérialisation JSON / XML / CSV | Test d’intégration | Les écarts de format sur le fil sont difficiles à repérer avec un fake |
| Routage, model binding, filtres, middleware | Test d’intégration | Vérification de la connexion avec le framework |
| Transitions d’état des ViewModel ou Presenter WPF / WinForms | Test unitaire | Garde son sens sans faire tourner l’UI |
| Binding réel, Dispatcher, cycle de vie des contrôles, boucle de messages | Test d’intégration ou test UI | Le vrai sujet est le comportement du framework et des threads |
| Chemins de fichiers, autorisations, verrous, dossiers partagés, fins de ligne, encodages de caractères | Test d’intégration | Nécessite le comportement réel de l’OS et du système de fichiers |
| Enregistrement COM, 32 bits / 64 bits, STA / MTA, source de chargement des DLL | Test d’intégration | Le vrai sujet est la différence d’environnement et la frontière de processus |
| Démarrage de l’application entière, vérification bout en bout des principaux cas d’usage | E2E / smoke | Un petit nombre suffit |
L’astuce pour lire ce tableau est de se demander quel test est le plus proche de « la raison pour laquelle ça casse en production ». Décider en fonction de l’incertitude que l’on veut réduire, plutôt qu’en fonction de l’emplacement du code, évite de dériver.
4. Ce que les tests unitaires doivent porter
Ce qui convient aux tests unitaires, c’est une responsabilité dont le sens subsiste une fois le monde extérieur retiré.
Par exemple :
- Règles métier
- Branchements
- Transitions d’état
- Validation des entrées
- Classification des erreurs
- Décision de la politique de retry
- Changements d’état du ViewModel / Presenter
- La logique de conversion elle-même
En particulier, plus il y a de combinaisons, plus il est intéressant de pousser vers les tests unitaires.
Par exemple, à mesure que les conditions de branchement se multiplient —
- coupon présent / absent
- en stock / hors stock
- première commande / commande répétée
- administrateur / utilisateur standard
- valeur normale / valeur limite / valeur invalide
— les faire toutes tourner via des tests d’intégration devient lourd. Il est plus rationnel de les découper finement avec des tests unitaires.
Par ailleurs, il est important, dans les tests unitaires, de garder les facteurs externes maîtrisables.
- Injecter l’heure actuelle
- Rendre les GUID et les nombres aléatoires remplaçables
- Ne pas attendre avec des sleep
- Ne pas toucher une vraie base de données ni de vrais fichiers
- Ne pas sortir sur le vrai réseau
Quand ces règles sont respectées, les tests deviennent nettement plus stables.
4.1. Quand les mocks se multiplient dans un test unitaire
Si, en essayant d’écrire un test unitaire, on se retrouve avec :
- 7 mocks nécessaires
- un setup long
- un arrange plus long que le corps du test
- l’impossibilité de voir ce que l’on veut vérifier
c’est généralement l’un des deux cas suivants :
- La classe a trop de responsabilités
- On pousse dans le test unitaire un câblage qui devrait en réalité être vérifié par un test d’intégration
Le mock est un outil pour couper le monde extérieur, pas un outil pour prouver que la connexion avec le vrai composant est correcte. Confondre les deux favorise le scénario où « tout est vert alors que ça plante en production ».
5. Les quatre frontières à faire monter vers les tests d’intégration
Les endroits qui méritent d’être élevés au rang de test d’intégration se classent globalement en quatre : le format, le câblage, l’environnement et le temps.
5.1. La frontière du format
Le format, ici, englobe des choses comme :
- JSON / XML / CSV
- Le schema et le mapping de la base de données
- nullable / precision / timezone
- La sérialisation des enum et des dates
- Les encodages de caractères et le BOM
- Les fins de ligne
Martin Fowler cite également les frontières impliquant serialize / deserialize comme candidates aux tests d’intégration. The Practical Test Pyramid est une bonne référence sur ce point.
Par exemple, des défauts comme
- un DTO sérialisé en JSON ressort avec des noms de champs différents,
- les guillemets ou les retours à la ligne d’un CSV sont cassés,
- un
decimala été arrondi, - le traitement de
DateTimeOffsetdans la base de données a dérivé, ou nullet la chaîne vide se sont comportés différemment de ce qui était prévu,
passent facilement à travers les tests unitaires seuls.
5.2. La frontière du câblage
La frontière du câblage englobe par exemple les parties suivantes :
- L’enregistrement DI
- Le bind de la configuration
- Le routage
- Le model binding
- Les filtres
- Le middleware
- Le démarrage du host
- Le câblage des événements
- Le Binding WPF et la connexion des commandes
Ici, le sujet n’est pas « ma fonction est-elle correcte ? » mais plusieurs composants réels sont-ils correctement connectés ?
Sous ASP.NET Core, la documentation officielle indique de resserrer le test unitaire d’une action de controller sur les décisions de l’action, et de traiter routing, model binding et filters du côté des tests d’intégration.
Le raisonnement est le même hors du web : dans une application desktop aussi, les transitions d’état du ViewModel relèvent du test unitaire, tandis que le comportement impliquant le vrai Binding XAML ou le Dispatcher penche vers le test d’intégration.
5.3. La frontière de l’environnement
Dans le développement Windows, ce point compte énormément.
- Les autorisations de fichiers
- Les dossiers partagés
- Le verrouillage de fichiers
- Le rename depuis un fichier temporaire
- Les privilèges administrateur
- Les autorisations de démarrage de service
- L’enregistrement COM
- Le 32 bits / 64 bits
- STA / MTA
- La source de chargement des DLL
Sur ces points, ce sont les conditions de l’OS et de l’environnement d’exécution eux-mêmes qui tiennent le premier rôle. Un fake en mémoire fait perdre une grande partie du sens, donc il est plus sûr de les couvrir par des tests d’intégration.
En particulier, dans les configurations impliquant un logiciel Windows existant ou COM / ActiveX, il est tout à fait courant de trébucher sur l’enregistrement, la bitness, le modèle de thread et les autorisations avant même que la logique n’entre en jeu. Ce type d’échec relève du domaine que couvrent les tests d’intégration incluant l’environnement, pas les tests unitaires.
5.4. La frontière du temps
Un autre point facile à négliger est le temps et la concurrence.
- Les timeouts
- L’annulation (cancellation)
- Le comportement réel des retry
- Le traitement piloté par timer
- L’arrêt des traitements en arrière-plan
- Les race conditions
- L’ordre de terminaison lors du shutdown
Ce qui compte ici, c’est de séparer la décision du comportement réel.
Par exemple,
- combien de fois réessayer, et
- quelles exceptions sont éligibles au retry
sont parfaitement couverts par des tests unitaires. En revanche,
- si le timeout se déclenche réellement,
- si l’annulation se propage,
- si tout survit à une collision entre un timer et un traitement asynchrone, et
- si les handles et les tâches se referment proprement à l’arrêt
penchent vers les tests d’intégration.
6. Erreurs de jugement fréquentes
6.1. Se satisfaire d’avoir mocké le Repository
Même si tout ce qui entoure le Repository passe avec des mocks, on ne sait toujours pas
- si le SQL est correct,
- si les transactions prennent effet,
- si cela correspond au schema,
- si le mapping ne dérive pas, ou
- si l’encodage et la precision restent intacts.
Le Repository est souvent moins une cible de test de logique qu’un point de connexion à une frontière. Dans ce cas, augmenter le poids des tests d’intégration par rapport aux tests unitaires correspond mieux à la réalité.
6.2. Vouloir couvrir jusqu’au framework dans un test unitaire de Controller / Endpoint
Ce que l’on veut voir dans un test unitaire d’action de controller, c’est essentiellement
- le branchement conditionnel,
- le choix de la valeur de retour, et
- quel service dépendant est appelé selon le cas.
En revanche,
- si la route correspond,
- si le model binding passe,
- si le filter prend effet, et
- à quoi ressemble le résultat après passage dans le middleware
relèvent du côté des tests d’intégration. Mélanger les deux rend difficile de savoir ce qui a cassé.
6.3. Faire l’exploration exhaustive des motifs d’entrée dans les tests d’intégration
Les tests d’intégration, étant plus proches du réel, sont inévitablement plus lents. Il est donc avantageux de séparer : l’exploration exhaustive des branches va aux tests unitaires, les cas représentatifs de la frontière vont aux tests d’intégration.
L’explication de Microsoft Learn sur les tests d’intégration recommande également, pour la base de données et le système de fichiers, non pas de faire tourner tous les motifs en test d’intégration, mais de se resserrer sur des scénarios représentatifs comme read / write / update / delete.
6.4. Frapper directement les systèmes de production des services externes depuis la CI
Il est plus sûr d’éviter cela.
Le réalisme compte dans un test d’intégration, mais cela ne signifie pas qu’il faille frapper le SaaS de production ou l’API de production à chaque exécution. Fowler recommande lui aussi de faire tourner les services externes en local, d’y placer un fake, ou d’utiliser une instance de test dédiée.
En pratique, une combinaison de
- base de données locale,
- répertoire temporaire,
- test host,
- environnement de test dédié, et
- fake service au contrat figé
est facile à manier.
7. Une structure recommandée en pratique
Il n’existe pas de ratio absolument correct. Cependant, la structure à trois couches suivante est largement applicable.
| Couche | Priorité | Ce qu’on y place |
|---|---|---|
| Couche cœur | Tests unitaires en grand nombre | Règles métier, transitions d’état, validation des entrées, classification des erreurs |
| Couche frontière | Tests d’intégration étroits | Base de données, fichiers, HTTP, serializer, DI, configuration, COM, autorisations |
| Couche globale | Un petit nombre de smoke / E2E | Vérification de démarrage, flux principaux, prévention de la récurrence d’incidents graves |
Intuitivement, c’est en nombre que les tests unitaires s’épaississent, et c’est en densité de frontière que les tests d’intégration s’épaississent.
La démarche recommandée est la suivante.
- D’abord, énumérer les frontières de l’application
- Façonner la logique de façon à pouvoir la couper du monde extérieur
- Pour chaque frontière, placer « au moins un happy path » et « un failure path représentatif »
- Limiter le nombre de vérifications bout en bout globales
- Quand un bug apparaît, ajouter un test à la couche capable de reproduire ce bug au moindre coût
Le point 5, le dernier, est le plus important.
- Si c’est une erreur de règle, ajouter un test unitaire
- Si c’est une erreur de SQL / binding / configuration / autorisations / enregistrement, ajouter un test d’intégration
- Si c’est un incident impliquant le démarrage ou la distribution, ajouter un smoke ou un E2E
En augmentant les tests de cette façon, les responsabilités des tests dérivent moins facilement.
8. Cinq questions à se poser en dernier recours
Pour finir, voici cinq questions condensées pour vérifier en cas d’hésitation.
- Si l’on remplace par un fake en mémoire, le sens que l’on veut vérifier subsiste-t-il ?
- S’il subsiste, penchez vers un test unitaire.
- Quand ça casse, soupçonnerait-on plutôt les connexions ou la configuration que la logique ?
- Si oui, penchez vers un test d’intégration.
- La base de données / les fichiers / le serializer / le DI / la route / le model binding / l’OS / les autorisations / la bitness / les threads sont-ils le vrai sujet ?
- Si oui, penchez vers un test d’intégration.
- Veut-on faire tourner rapidement un grand nombre de motifs d’entrée ?
- Si oui, penchez vers un test unitaire.
- Quand ce test échoue, sait-on immédiatement quoi corriger ?
- Si non, les couches de test sont mélangées.
En s’organisant avec ces cinq questions, on évite plus facilement les décisions approximatives du genre « test d’intégration parce que c’est vaguement plus proche du réel » ou « test unitaire parce que c’est vaguement plus rapide ».
9. Conclusion
La frontière entre tests unitaires et tests d’intégration se décide de la façon la plus pratique non pas selon l’emplacement du code, mais selon l’incertitude que l’on veut réduire.
L’essentiel se résume à ces cinq points.
- Le test unitaire est un test de décision
- Le test d’intégration est un test de connexion
- L’exploration exhaustive des branches va aux tests unitaires
- Le format, le câblage, l’environnement et le temps vont aux tests d’intégration
- Couvrir la vérification bout en bout globale avec un petit nombre de smoke / E2E
Ce qu’il faut le plus éviter, ce sont ces trois choses :
- Croire qu’un mock a prouvé jusqu’à la connexion avec le vrai composant
- Essayer de faire tourner toutes les branches en test d’intégration
- Mélanger les responsabilités des tests unitaires et des tests d’intégration
En cas d’hésitation, commencez par vous demander si ce défaut casse une « décision » ou une « connexion ». Cette seule question permet de trancher une grande part des cas.
10. Articles connexes
- Une checklist minimale de sécurité pour le développement d’applications Windows
- Jusqu’où peut-on vraiment faire d’une application Windows un binaire unique ?
- Quand a-t-on vraiment besoin des privilèges administrateur sous Windows ?
- Qu’est-ce que Reg-Free COM ?
11. Références
-
Microsoft Learn, Integration tests in ASP.NET Core ↩
-
Microsoft Learn, Unit testing best practices for .NET ↩
-
Martin Fowler, The Practical Test Pyramid ↩
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Exigences minimales pour un logger maison, avec une checklist de tests d'intégration
Pour rendre fiables les journaux de diagnostic d'une application maison, nous détaillons le format UTF-8 JSON Lines, les champs obligatoi...
Accélérer la validation des applications avec Windows Sandbox
Comment utiliser Windows Sandbox pour isoler les problèmes de privilèges administrateur, reproduire des anomalies dans un environnement p...
Un tableau de décision pour choisir entre arrêt et poursuite après une exception inattendue
Lorsqu'une exception inattendue survient, faut-il arrêter l'application ou la laisser continuer ? Cet article organise la décision sous l...
Comment isoler concrètement, dans une application Windows, « uniquement les opérations nécessitant des privilèges administrateur »
Comment concevoir une application Windows qui garde son UI en asInvoker tout en isolant dans un helper EXE les seuls traitements nécessit...
Stocker les secrets des applications Windows - Éviter les configurations en clair avec DPAPI
Pour éviter de stocker en clair, dans un fichier de configuration, les informations de connexion et les jetons API d'une application Wind...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Conseil technique et revue de conception
Décider où placer la frontière entre tests unitaires et tests d'intégration se prête bien à une revue de conception avant implémentation ou à une consultation sur la stratégie de test.
Développement d'applications Windows
Dans les applications Windows, des frontières comme les fichiers, les autorisations, COM ou le 32 bits / 64 bits se répercutent directement sur les couches de test, ce qui se prête bien à la clarification de l'approche d'implémentation.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Comment choisir entre un test unitaire et un test d'intégration ?
- En un mot, le test unitaire est un « test de décision » et le test d'intégration un « test de connexion ». Placez du côté des tests unitaires tout ce dont le sens est complet sans ressource externe, comme le calcul de montants, les transitions d'état, la validation des entrées ou la classification des exceptions ; placez du côté des tests d'intégration tout ce qui « vous trahit dès qu'on le connecte », comme l'exécution SQL, la sérialisation JSON/CSV, le routage, l'enregistrement DI, le verrouillage de fichiers, les autorisations, l'enregistrement COM ou le 32 bits/64 bits. Si les deux permettent de vérifier la même chose, commencez toujours par un test unitaire.
- Qu'est-ce qui devrait être élevé au rang de test d'intégration ?
- On peut globalement les classer en quatre frontières : le format, le câblage, l'environnement et le temps. Le format couvre JSON/CSV, le mapping vers la base de données, l'encodage des caractères, etc. Le câblage couvre l'enregistrement DI, le routage, le model binding et la connexion d'autres composants réels. L'environnement couvre le comportement réel de l'OS, comme les autorisations de fichiers, l'enregistrement COM, le 32 bits/64 bits ou STA/MTA. Le temps couvre le timeout, l'annulation, les race conditions, etc. Comme ces aspects perdent leur sens avec un fake en mémoire, il est plus sûr de les couvrir par des tests d'intégration.
- Quel est le problème quand un test unitaire nécessite trop de mocks ?
- Si vous avez besoin de 7 mocks, que le setup est long, ou que vous ne voyez plus ce que vous cherchez à vérifier, c'est généralement l'un de ces deux problèmes : soit la classe a trop de responsabilités, soit vous poussez dans le test unitaire un câblage qui devrait en réalité être vérifié par un test d'intégration. Un mock est un outil pour couper le monde extérieur, pas un outil pour prouver que la connexion avec le vrai composant est correcte. Confondre les deux favorise le scénario où « tout est vert alors que ça plante en production ».
- Avec quelle structure et quel ratio faut-il organiser ses tests ?
- Une structure à trois couches est largement applicable. La couche cœur porte massivement les règles métier et les transitions d'état en tests unitaires ; la couche frontière place des tests d'intégration étroits sur la base de données, les fichiers, le serializer, le DI, etc. ; la couche globale couvre les vérifications de démarrage et les flux principaux avec un petit nombre de tests smoke/E2E. Faites tourner l'exploration exhaustive des branches en tests unitaires, et limitez les tests d'intégration à au moins un happy path et un failure path représentatif par frontière. Quand un bug apparaît, ajoutez le test à la couche capable de le reproduire au moindre coût.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics