Les tests unitaires sont une méthode de vérification logicielle où l'on teste la correction de modules individuels ou de fonctions du code en isolation du reste du système. Selon Martin Fowler, 2026, les tests unitaires sont le fondement de l'IC/CD et du refactoring, fournissant un retour rapide sur le fonctionnement du code. Les tests modulaires aident à détecter les erreurs dès les premières étapes du développement, réduisant considérablement le coût de leur correction.
Points clés
Un test unitaire est le processus de vérification d'unités individuelles du code source — fonctions, méthodes, classes — en isolation du reste du programme. Chaque test exécute un scénario d'utilisation spécifique du module et vérifie que le résultat correspond à celui attendu. Les tests unitaires sont écrits dans le même langage de programmation que le code principal et s'exécutent automatiquement dans l'environnement de développement ou dans un pipeline CI/CD. Contrairement aux tests d'intégration, les tests unitaires n'interagissent pas avec des bases de données réelles, des systèmes de fichiers ou des services réseau.
L'objectif principal est un retour rapide sur la correction du code après des modifications. Si un développeur refactorise une méthode, la suite de tests unitaires confirme que le comportement n'a pas été altéré. Selon Google Testing Blog (2025), les projets avec une couverture de tests unitaires supérieure à 60% ont 2,5 fois moins d'incidents en production. Avantages supplémentaires : documentation du code (les tests montrent comment utiliser l'API), simplification du refactoring (l'implémentation peut être modifiée tout en conservant le comportement) et diagnostic rapide des régressions.
Ce n'est pas tout test automatisé qui est un test unitaire. Critères : un seul module (classe ou fonction) est testé, les dépendances externes sont remplacées par des mocks ou des stubs, le test s'exécute en millisecondes et ne nécessite pas de démarrer un serveur ou une base de données. Un test qui accède à une base de données réelle est un test d'intégration. Un test qui ouvre un navigateur est un test E2E. Comprendre les limites entre les types de tests est important pour répartir correctement les efforts dans la pyramide des tests.
Les tests unitaires de qualité suivent les principes FIRST formulés par Robert C. Martin. Chaque test doit être Fast (rapide — millisecondes), Isolated (isolé — ne dépend pas d'autres tests), Repeatable (reproductible — même résultat sur n'importe quelle machine), Self-validating (auto-validant — résultat « passed » ou « failed », sans vérification manuelle) et Timely (opportun — écrit avant ou simultanément au code). Violer un principe réduit la valeur du test.
Un modèle standard pour écrire des tests unitaires. Arrange — préparation des données et dépendances : création d'objets, configuration des mocks, définition des paramètres d'entrée. Act — exécution de l'action testée : appel d'une méthode ou fonction. Assert — vérification du résultat : comparaison de la valeur réelle avec la valeur attendue. La division en trois blocs rend le test lisible et compréhensible. Si le bloc Assert nécessite une logique complexe, le test vérifie probablement trop de choses à la fois.
// Exemple de test unitaire avec le pattern AAA en Kotlin avec JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — création de l'objet de test
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — exécution de l'action
val result = calculator.add(2, 3)
// ASSERT — vérification du résultat
Assertions.assertEquals(5, result)
}
}
Le nom du test doit décrire ce qui est vérifié et quel résultat est attendu. Format : [methodName]_[scenario]_[expectedResult]. Exemple : calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Un bon nom de test remplace un commentaire et, en cas d'échec, indique immédiatement quelle fonctionnalité est cassée. Évitez les noms comme test1, checkSomething ou verify — ils n'apportent pas d'information et compliquent le diagnostic.
Pour isoler le module testé des dépendances externes, on utilise des doubles de test (test doubles). Principaux types : mocks — vérifient qu'une méthode spécifique a été appelée avec les bons paramètres ; stubs — retournent des valeurs prédéfinies lors de l'appel d'une méthode ; fakes — implémentations simplifiées de composants réels (par exemple, InMemoryUserRepository au lieu de UserRepository qui travaille avec une BD). Le choix dépend de ce qui doit être vérifié : l'état (stub) ou l'interaction (mock).
| Double | Ce qu'il vérifie | Exemple |
|---|---|---|
| Mock | Appel de méthode avec les bons paramètres | userRepository.save(user) a été appelé exactement 1 fois |
| Stub | Valeur de retour | repository.findById(1) retourne User(id=1, name="Test") |
| Fake | Logique via implémentation simplifiée | InMemoryMapUserRepository avec HashMap au lieu de BD |
| Spy | Mocking partiel d'un objet réel | spy(repo).when(findById).thenReturn(user) |
Mockito est le framework de mocking le plus populaire pour Java et Kotlin. Il permet de créer des mocks via mock(), de configurer des valeurs de retour via when().thenReturn() et de vérifier les appels via verify(). Les versions modernes de Mockito (5.x) prennent en charge les mocks statiques (mockStatic) et une syntaxe simplifiée via BDDMockito (given-willReturn). Une règle importante : ne mockez pas ce qui ne vous appartient pas — ne créez pas de mocks pour les objets valeurs et les bibliothèques standard.
// Exemple de test unitaire avec Mockito en Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) est une méthodologie où le test est écrit avant l'implémentation du code. Le cycle « Rouge-Vert-Refactor » : écrire un test qui échoue (Rouge), écrire le code minimal pour passer le test (Vert), améliorer le code sans changer le comportement (Refactor). TDD garantit que tout le code est couvert par des tests (couverture = 100% pour les fonctionnalités implémentées) et que le code est testable — si le code est difficile à tester, l'architecture doit être améliorée.
Selon une étude d'IBM (2006-2026, étude longitudinale), les équipes utilisant TDD ont 40 à 80% moins de défauts en production par rapport aux équipes qui écrivent les tests après le code. TDD améliore également l'architecture : le développeur est obligé de penser à la conception de l'API avant l'impléparation, ce qui conduit à un couplage faible (loose coupling) et une forte cohésion (high cohesion). Un effet supplémentaire est la documentation vivante : les tests servent de spécification du comportement du module, toujours à jour.
TDD n'est pas toujours optimal. Les composants d'interface utilisateur sont difficiles à tester isolément — les tests snapshot ou les tests de régression visuelle (Percy, Chromatic) sont plus efficaces. Le prototypage et la recherche (spike solutions) ne nécessitent pas de tests. Le code legacy sans tests est difficile à couvrir via TDD — ici, des tests de caractérisation (tests qui capturent le comportement actuel avant le refactoring) sont d'abord nécessaires. Dans ces cas, TDD n'est pas complètement abandonné mais adapté — des tests sont écrits pour la fonctionnalité modifiée, pas pour l'ensemble du code legacy.
Le développement mobile a ses spécificités : la logique métier est souvent mélangée au code d'interface utilisateur (Activity, ViewController, ViewModel), ce qui complique les tests unitaires. La meilleure pratique est des vues fines, des ViewModels épais : extrayez toute la logique des composants d'interface utilisateur dans des classes séparées (UseCase, Repository, ViewModel) faciles à tester sans émulateur. Android et iOS ont des frameworks de tests unitaires natifs qui fonctionnent sur JVM/Native sans lancer d'appareil.
Les tests unitaires Android s'exécutent sur une JVM locale sans émulateur, offrant une vitesse d'exécution — un test typique prend moins de 100 ms. JUnit 5 est le runner principal. Pour les tests ViewModel, utilisez kotlinx-coroutines-test pour tester les coroutines et Turbine pour tester StateFlow. Robolectric permet de tester les composants dépendant d'Android (Context, Resources) sans émulateur en chargeant des shadow-classes. Pour les tests Compose, utilisez Compose UI Test — mais ce sont des tests d'interface utilisateur, pas des tests unitaires.
Les tests unitaires iOS sont écrits en Swift avec XCTest (intégré dans Xcode). Quick + Nimble sont des frameworks BDD pour des tests plus lisibles (describe/context/it). Pour le mocking, utilisez Cuckoo (génération de mocks) ou SwiftyMocky. Swift prend en charge les protocoles et l'injection de dépendances, facilitant le remplacement des dépendances. Point clé : les tests unitaires iOS s'exécutent sur le simulateur macOS, pas sur un appareil réel. Les tests nécessitant des fonctions matérielles (appareil photo, Bluetooth) sont des tests d'intégration.
Les tests unitaires Flutter utilisent le package flutter_test et s'exécutent sur Dart VM sans émulateur. Pour le mocking, utilisez le package mockito avec génération de code (build_runner). Les tests de widgets (dans le même package) testent des widgets individuels mais nécessitent du rendu et sont plus lents — utilisez-les uniquement pour la vérification de la logique d'interface utilisateur. La logique Dart pure (modèles, référentiels, blocs) est testée comme des tests Dart normaux sans importer flutter_test.
// Exemple de test unitaire sur Flutter avec mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Les tests unitaires efficaces exigent de la discipline. La règle principale : testez le comportement, pas l'implémentation. Le test ne doit pas savoir comment le module est implémenté en interne (quelles méthodes privées sont appelées, dans quel ordre). Si un test est lié à l'implémentation, il se brise à chaque refactorisation et perd sa valeur. Le test vérifie le contrat : pour une entrée X, la sortie doit être Y. L'exception concerne les tests d'algorithmes aux performances critiques, où l'ordre des appels est important.
Une couverture de 100% est un objectif inaccessible et inutile. Selon Google Testing Blog (2025), le niveau de couverture optimal pour les tests unitaires est de 70 à 80% des lignes de code. Une couverture de 100% est souvent atteinte en testant les getters, setters et constructeurs, ce qui n'apporte aucune valeur. Concentrez-vous sur la logique métier critique : calculs complexes, validation, gestion des erreurs, cas limites. Utilisez JaCoCo (Java), Coverage.py (Python), Istanbul (JS) pour la mesure et définissez un seuil dans le CI — échec de build en dessous de 60% de couverture.
Les tests unitaires sont la première étape de tout pipeline CI/CD. Ils s'exécutent à chaque push dans le dépôt, avant la construction et le déploiement. Le temps d'exécution moyen de la suite de tests unitaires ne doit pas dépasser 5 minutes — au-delà, les tests cessent d'être « rapides » et les développeurs cessent de les exécuter localement. Séparez les tests en rapides (unitaires) et lents (intégration) et exécutez-les dans différentes étapes du pipeline. Utilisez l'exécution parallèle et le fail-fast pour accélérer.
Questions fréquentes
Un test unitaire vérifie un seul module isolément, en remplaçant les dépendances externes par des mocks. Un test d'intégration vérifie l'interaction entre plusieurs composants réels (BD, API, système de fichiers). Les tests unitaires s'exécutent en millisecondes, les tests d'intégration en secondes. Dans la pyramide des tests, les tests unitaires occupent 70%.
Le choix dépend de la plateforme : JUnit 5 pour Java/Kotlin, XCTest pour iOS/Swift, pytest pour Python, Jest/Vitest pour JavaScript/TypeScript, flutter_test pour Flutter. Pour le mocking, utilisez Mockito (Java), Cuckoo (iOS), unittest.mock (Python) ou vitest.mock (JS). Tous les frameworks modernes prennent en charge les tests paramétrés, les assertions intégrées et l'exécution parallèle.
Fast — le test s'exécute en millisecondes. Isolated — il ne dépend pas d'autres tests ou systèmes externes. Repeatable — donne le même résultat sur n'importe quelle machine. Self-validating — vérifie automatiquement le résultat. Timely — écrit avant ou en même temps que le code. La violation d'un seul principe réduit l'efficacité des tests.
Oui, absolument. Le ViewModel contient la logique métier — gestion des événements, transformation des données, gestion d'état. Sur Android, utilisez kotlinx-coroutines-test pour les coroutines et Turbine pour tester StateFlow. Sur iOS, testez les Combine Publishers ou async/await dans le ViewModel. Les tests ViewModel sont de purs tests unitaires qui s'exécutent sur JVM/macOS sans émulateur.
Les requêtes réseau dans les tests unitaires ne sont pas exécutées — elles sont remplacées par des mocks du client HTTP. Sur Android, utilisez MockWebServer (OkHttp) — il lance un serveur HTTP local, préférable aux mocks car il reproduit l'interaction réseau réelle. MockWebServer offre l'isolation sans perdre le réalisme. Pour iOS — OHHTTPStubs ou URLProtocol pour intercepter et remplacer les réponses.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi