Où tracer la frontière entre tests unitaires et tests d'intégration

· Mis à jour le: · · 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 :

  1. La logique pure va dans les tests unitaires
  2. Les connexions, le câblage, les conversions et les différences d’environnement vont dans les tests d’intégration
  3. Si les deux permettent de vérifier, commencez par un test unitaire
  4. 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 :

  1. La classe a trop de responsabilités
  2. 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 decimal a été arrondi,
  • le traitement de DateTimeOffset dans la base de données a dérivé, ou
  • null et 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.

  1. D’abord, énumérer les frontières de l’application
  2. Façonner la logique de façon à pouvoir la couper du monde extérieur
  3. Pour chaque frontière, placer « au moins un happy path » et « un failure path représentatif »
  4. Limiter le nombre de vérifications bout en bout globales
  5. 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.

  1. 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.
  2. 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.
  3. 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.
  4. Veut-on faire tourner rapidement un grand nombre de motifs d’entrée ?
    • Si oui, penchez vers un test unitaire.
  5. 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

11. Références

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

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.

Retour au blog