Projet pédagogique qui illustre, étape par étape, comment tester du code Java avec JUnit 5, Mockito et Spring Boot Test.
L'objectif n'est pas de tester une « vraie » application, mais de rendre visibles les mécanismes : mock, spy, injection, cycle de vie, assertions et pièges courants.
Trois couches qui se délèguent le travail, du plus haut niveau au plus bas :
CalculatorService ──► MathHelper ──► ToMockHelper
(service) (logique) (valeurs fixes)
ToMockHelperrenvoie des valeurs fixes (10,20,10 + 20). C'est le candidat parfait à « remplacer » dans un test.MathHelperfait de l'arithmétique et délègue àToMockHelper.CalculatorServiceest le service exposé ; il délègue àMathHelper.
Cette chaîne de dépendances est ce qui permet d'illustrer proprement les mocks : on teste une couche en remplaçant celle du dessous.
Il y a deux pistes parallèles qui enseignent EXACTEMENT les mêmes notions
(A → E, à lire dans l'ordre) :
- 🧮 Piste
Brutal(les maths) — packagecom.sbp.testing: des nombres nus (add(2,3)=5). Sans détour, mais abstrait. Fichiers préfixés*_Brutal_*. - ☕ Piste
Soft(le café) — packagecom.sbp.testing.soft: une histoire de café (un café 3 €, un croissant 2 €, une formule 5 €, une caisse, un catalogue de prix). Commencez par celle-ci si les nombres nus vous semblent abstraits : les valeurs ont enfin un sens. Fichiers préfixés*_Soft_*.
Le nom de chaque classe annonce donc sa piste : A_Brutal_ExempleSimple (maths)
et A_Soft_ExempleConcret (café) enseignent la même chose. Choisissez celle qui
vous parle ; chaque fichier A–E a son équivalent dans l'autre piste.
| Notion | Piste abstraite | Piste concrète (café) | Spring ? |
|---|---|---|---|
Bases : mock, spy, assertions, pièges, assertThrows, verify, AssertJ |
A_Brutal_ExempleSimple |
A_Soft_ExempleConcret |
❌ |
| Cycle de vie JUnit + piège du spy non câblé (✅/❌) | B_Brutal_BeforeAndAfter |
B_Soft_CycleDeVie |
✅ (@Autowired) |
@Mock + @InjectMocks, matchers, ArgumentCaptor |
C_Brutal_Injected |
C_Soft_Injection |
❌ (MockitoExtension) |
Mock partiel avec @Spy : vrai code vs stub |
D_Brutal_Spy |
D_Soft_Espion |
❌ (MockitoExtension) |
Intégration Spring : @MockitoBean remplace un bean |
E_Brutal_MockitoBean |
E_Soft_BeanRemplace |
✅ (obligatoire) |
CommandeService ──► Caisse ──► CataloguePrix
(le client) (calcule) (prix externes : BD/API -> à mocker)
Le CataloguePrix illustre POURQUOI on mocke : en vrai ses prix viennent d'une
base de données ou d'une API qu'on ne veut pas appeler dans un test. Et la
méthode Caisse.encaisser(...) a un effet de bord visible (« Encaissement
RÉEL ») : elle sert à montrer, en direct dans la console, le danger du when()
sur un espion (voir A_Soft_ExempleConcret#espion_pourquoiDoReturn).
- Mock : coquille vide. Toutes les méthodes renvoient une valeur par défaut
(
0,null,false) tant qu'on ne les stubbe pas. - Spy : objet réel enveloppé. Il exécute le vrai code, sauf pour les méthodes qu'on stubbe explicitement (« mock partiel »).
Sur un spy, préférez doReturn(...).when(spy).methode(...) plutôt que
when(spy.methode(...)).thenReturn(...) : la seconde forme appelle réellement
spy.methode(...) pendant le stubbing, ce qui peut déclencher des effets de bord.
Voir A_Brutal_ExempleSimple#spy_pourquoiDoReturn.
@SpringBootTest démarre tout le contexte Spring : c'est lent. Ne l'utilisez
que lorsque vous testez réellement l'intégration Spring (@Autowired,
@MockitoBean). Pour un simple test de logique avec des mocks, préférez
@ExtendWith(MockitoExtension.class) (voir C et D).
./mvnw testA_Brutal_ExempleSimple#echoueVolontairement et A_Soft_ExempleConcret#echoueVolontairement).
C'est intentionnel : ils sont là pour vous faire voir à quoi ressemble un échec et
pourquoi une assertion fausse casse la construction. Ils sont isolés et clairement
étiquetés (❌ dans leur @DisplayName).