{"id":36,"date":"2026-06-28T16:37:32","date_gmt":"2026-06-28T14:37:32","guid":{"rendered":"https:\/\/missud.eu\/index.php\/2026\/06\/28\/dojo-tdd-boite-noire-vs-boite-blanche-tester-le-comportement-ou-limplementation\/"},"modified":"2026-06-28T16:41:21","modified_gmt":"2026-06-28T14:41:21","slug":"dojo-tdd-boite-noire-vs-boite-blanche-tester-le-comportement-ou-limplementation","status":"publish","type":"post","link":"https:\/\/missud.eu\/index.php\/2026\/06\/28\/dojo-tdd-boite-noire-vs-boite-blanche-tester-le-comportement-ou-limplementation\/","title":{"rendered":"Dojo TDD \u2014 Bo\u00eete Noire vs Bo\u00eete Blanche : tester le comportement ou l&rsquo;impl\u00e9mentation ?"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Un dojo de code, c&rsquo;est une s\u00e9ance d&rsquo;entra\u00eenement d\u00e9lib\u00e9r\u00e9 : un probl\u00e8me bien d\u00e9fini, des contraintes explicites, et un d\u00e9brief qui fait r\u00e9fl\u00e9chir. Celui-ci tourne autour d&rsquo;un syst\u00e8me de biblioth\u00e8que et d&rsquo;une question fondamentale : <strong>quand on \u00e9crit des tests, est-ce qu&rsquo;on teste un comportement ou une impl\u00e9mentation ?<\/strong> Les repos sont disponibles sur GitHub : <a href=\"https:\/\/github.com\/dmissud\/black-box\">black-box<\/a> et <a href=\"https:\/\/github.com\/dmissud\/white-box\">white-box<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Le syst\u00e8me \u2014 une biblioth\u00e8que en architecture hexagonale<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le domaine est volontairement simple. Trois concepts m\u00e9tier :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Member<\/strong> \u2014 un abonn\u00e9, typ\u00e9 <code>CHILD<\/code>, <code>ADULT<\/code> ou <code>PREMIUM<\/code><\/li>\n<li><strong>Book<\/strong> \u2014 un livre, cat\u00e9goris\u00e9 <code>YOUTH<\/code>, <code>STANDARD<\/code> ou <code>PREMIUM<\/code><\/li>\n<li><strong>Loan<\/strong> \u2014 un emprunt en cours, avec date de d\u00e9but, date d&rsquo;\u00e9ch\u00e9ance et date de retour optionnelle<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">L&rsquo;architecture suit le pattern ports &amp; adapters : le domaine expose des interfaces (<code>MemberUseCase<\/code>, <code>BookUseCase<\/code>, <code>LoanUseCase<\/code>) et des ports de sortie (<code>MemberRepository<\/code>, <code>BookRepository<\/code>, <code>LoanRepository<\/code>). L&rsquo;infrastructure fournit des impl\u00e9mentations H2 en m\u00e9moire. Le domaine ne d\u00e9pend de rien d&rsquo;externe.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Des tests d&rsquo;int\u00e9gration (<code>LibraryIntegrationTest<\/code>) couvrent d\u00e9j\u00e0 les fonctionnalit\u00e9s de base : enregistrer un membre, ajouter un livre au catalogue, emprunter, retourner, lister les emprunts actifs. Ces tests passent au d\u00e9part \u2014 c&rsquo;est la base saine sur laquelle le dojo va construire.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">La mission \u2014 impl\u00e9menter une politique d&rsquo;acc\u00e8s aux emprunts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le <code>LoanService<\/code> de d\u00e9part est na\u00eff : il emprunte sans aucune v\u00e9rification m\u00e9tier. La mission du dojo est d&rsquo;impl\u00e9menter trois r\u00e8gles dans <code>LoanAccessPolicyTest<\/code> en suivant le cycle RED \u2192 GREEN \u2192 REFACTOR :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>R\u00e8gle 1 \u2014 Acc\u00e8s par cat\u00e9gorie<\/strong> : YOUTH est accessible \u00e0 tous ; STANDARD aux ADULT et PREMIUM ; PREMIUM aux PREMIUM uniquement.<\/li>\n<li><strong>R\u00e8gle 2 \u2014 Quota d&#8217;emprunts simultan\u00e9s<\/strong> : 2 pour CHILD, 5 pour ADULT, 10 pour PREMIUM.<\/li>\n<li><strong>R\u00e8gle 3 \u2014 Dur\u00e9e maximale<\/strong> : 21 jours \u00e0 partir d&rsquo;aujourd&rsquo;hui, pas un de plus.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">En cas de violation, une exception m\u00e9tier d\u00e9di\u00e9e doit \u00eatre lev\u00e9e : <code>BookAccessDeniedException<\/code>, <code>LoanQuotaExceededException<\/code>, <code>LoanDurationExceededException<\/code>. Les deux groupes re\u00e7oivent exactement la m\u00eame consigne, mais avec une contrainte diff\u00e9rente sur la fa\u00e7on d&rsquo;\u00e9crire les tests.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bo\u00eete noire \u2014 tester le contrat, ignorer l&rsquo;int\u00e9rieur<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La consigne bo\u00eete noire est explicite : <em>\u00ab\u00a0Tu ne dois lire aucune classe des packages <code>application<\/code> ou <code>infrastructure<\/code>. Tu testes uniquement via les interfaces d\u00e9finies dans <code>domain\/port\/in<\/code>.\u00a0\u00bb<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En pratique, le setup de test monte une vraie base H2 en m\u00e9moire et instancie les services via leurs interfaces :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private MemberUseCase memberUseCase;\nprivate BookUseCase   bookUseCase;\nprivate LoanUseCase   loanUseCase;\n\n@BeforeEach\nvoid setUp() throws SQLException {\n    DatabaseConfig.initialize();\n    memberUseCase = new MemberService(new H2MemberRepository());\n    bookUseCase   = new BookService(new H2BookRepository());\n    loanUseCase   = new LoanService(...);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pour tester la r\u00e8gle 1, on cr\u00e9e r\u00e9ellement un membre CHILD et un livre STANDARD dans la base, puis on tente l&#8217;emprunt via <code>loanUseCase.borrow()<\/code> et on v\u00e9rifie l&rsquo;exception. Pas de mock, pas de stub : le syst\u00e8me entier tourne. La contrainte n&rsquo;est pas technique, elle est intellectuelle \u2014 on ne regarde pas le code du service pour savoir ce qu&rsquo;il fait, on d\u00e9duit le comportement attendu depuis les sp\u00e9cifications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">C&rsquo;est inconfortable au d\u00e9but. On ne sait pas si <code>LoanService<\/code> impl\u00e9mente d\u00e9j\u00e0 la r\u00e8gle ou pas. On \u00e9crit le test RED en aveugle, puis on lance <code>mvn test<\/code> pour voir. C&rsquo;est exactement \u00e7a, TDD.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Bo\u00eete blanche \u2014 tester vite, avec des mocks sur les classes concr\u00e8tes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La consigne bo\u00eete blanche ouvre tout : <em>\u00ab\u00a0Tu as acc\u00e8s \u00e0 tout le code source : mod\u00e8les, services, repositories. Tu peux mocker les repositories directement et instancier <code>LoanService<\/code> \u00e0 la main.\u00a0\u00bb<\/em> Elle ajoute une note p\u00e9dagogique : <em>\u00ab\u00a0Garde en t\u00eate que tu d\u00e9couvriras en fin de session pourquoi un trop fort couplage aux classes concr\u00e8tes peut devenir un probl\u00e8me.\u00a0\u00bb<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le setup est minimal gr\u00e2ce \u00e0 Mockito :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Mock private H2MemberRepository memberRepository;\n@Mock private H2BookRepository   bookRepository;\n@Mock private H2LoanRepository   loanRepository;\n\n@BeforeEach\nvoid setUp() {\n    loanService = new LoanService(loanRepository, memberRepository, bookRepository);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Chaque test configure exactement ce dont il a besoin. Pour la r\u00e8gle du quota, on stubble <code>findActiveByMemberId<\/code> pour retourner une liste de N emprunts existants, sans toucher la base :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Test\n@DisplayName(\"Un enfant ne peut pas d\u00e9passer 2 emprunts simultan\u00e9s\")\nvoid shouldRejectLoanWhenChildReachedMaxQuota() {\n    List&lt;Loan&gt; activeLoans = List.of(\n        new Loan(UUID.randomUUID(), memberId, UUID.randomUUID(), LocalDate.now(), LocalDate.now().plusDays(7)),\n        new Loan(UUID.randomUUID(), memberId, UUID.randomUUID(), LocalDate.now(), LocalDate.now().plusDays(7))\n    );\n    when(memberRepository.findById(memberId))\n        .thenReturn(Optional.of(new Member(memberId, \"Alice\", MemberType.CHILD)));\n    when(bookRepository.findById(bookId))\n        .thenReturn(Optional.of(new Book(bookId, \"Livre\", \"Auteur\", BookCategory.YOUTH)));\n    when(loanRepository.findActiveByMemberId(memberId)).thenReturn(activeLoans);\n\n    assertThrows(LoanQuotaExceededException.class, () -&gt;\n        loanService.borrow(memberId, bookId, LocalDate.now().plusDays(7))\n    );\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Les tests sont rapides, pr\u00e9cis, isol\u00e9s. Chaque test v\u00e9rifie exactement une chose, sans bruit. La couverture des cas limite est facile \u00e0 atteindre.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">La progression TDD \u2014 RED, GREEN, REFACTOR<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dans les deux cas, la progression suit le m\u00eame rythme. On commence par \u00e9crire un premier test RED sur la r\u00e8gle la plus simple \u2014 un CHILD qui tente d&#8217;emprunter un livre STANDARD. Le test \u00e9choue parce que le <code>LoanService<\/code> de d\u00e9part ne v\u00e9rifie rien. C&rsquo;est voulu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On impl\u00e9mente le minimum dans <code>LoanService.borrow()<\/code> pour faire passer ce test : r\u00e9cup\u00e9rer le membre, r\u00e9cup\u00e9rer le livre, comparer les types, lever l&rsquo;exception si n\u00e9cessaire. Le test passe (GREEN). On n&rsquo;en fait pas plus.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Puis on ajoute le test suivant \u2014 un ADULT qui tente un livre PREMIUM \u2014 et on \u00e9tend la r\u00e8gle. Puis la r\u00e8gle de quota. Puis la dur\u00e9e. \u00c0 chaque it\u00e9ration, les tests existants agissent comme un filet de s\u00e9curit\u00e9 : on ne peut pas casser une r\u00e8gle d\u00e9j\u00e0 impl\u00e9ment\u00e9e sans le voir imm\u00e9diatement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La phase REFACTOR arrive naturellement quand les conditions s&rsquo;accumulent dans <code>borrow()<\/code>. On peut extraire une m\u00e9thode <code>checkAccessPolicy()<\/code>, ou une classe d\u00e9di\u00e9e. Les tests restent verts : c&rsquo;est eux qui d\u00e9finissent ce que le refactoring doit pr\u00e9server.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Le d\u00e9brief \u2014 pourquoi \u00e7a compte vraiment<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">C&rsquo;est l\u00e0 que le dojo prend tout son sens. On compare les deux bases de tests face \u00e0 un sc\u00e9nario de refactoring : on renomme <code>H2MemberRepository<\/code> en <code>InMemoryMemberRepository<\/code>, ou on extrait la politique d&rsquo;acc\u00e8s dans un objet domaine d\u00e9di\u00e9.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les tests bo\u00eete blanche cassent imm\u00e9diatement. Ils mockaient <code>H2MemberRepository<\/code> par son nom de classe \u2014 le compilateur se plaint avant m\u00eame de lancer les tests. Pire : si on refactore <code>LoanService.borrow()<\/code> pour d\u00e9l\u00e9guer la v\u00e9rification \u00e0 un <code>LoanPolicy<\/code>, les mocks ne correspondent plus \u00e0 la nouvelle structure. Il faut r\u00e9\u00e9crire des tests qui testaient pourtant la m\u00eame r\u00e8gle m\u00e9tier.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les tests bo\u00eete noire ne bougent pas. Ils ne connaissent ni <code>H2MemberRepository<\/code> ni <code>LoanService<\/code>. Ils savent juste que <code>loanUseCase.borrow(childId, standardBookId, dueDate)<\/code> doit lever une exception. Ce contrat est stable \u2014 il est d\u00e9fini par la r\u00e8gle m\u00e9tier, pas par l&rsquo;impl\u00e9mentation du moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La note p\u00e9dagogique de la consigne bo\u00eete blanche prenait tout son sens \u00e0 ce moment : <em>\u00ab\u00a0un trop fort couplage aux classes concr\u00e8tes peut devenir un probl\u00e8me\u00a0\u00bb<\/em> \u2014 ce n&rsquo;\u00e9tait pas une mise en garde abstraite, c&rsquo;\u00e9tait une proph\u00e9tie.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Ce que j&rsquo;en retiens<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La bo\u00eete blanche permet d&rsquo;\u00e9crire des tests plus vite, avec plus de contr\u00f4le sur les cas limite. Elle est souvent plus confortable, surtout quand on d\u00e9bute avec TDD. Mais ce confort a un prix : les tests s&rsquo;attachent \u00e0 la structure interne, et chaque refactoring devient un risque de casse inutile.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La bo\u00eete noire demande un effort suppl\u00e9mentaire au d\u00e9part \u2014 comprendre et formuler le comportement attendu sans regarder l&rsquo;impl\u00e9mentation. Mais elle produit des tests qui durent, qui documentent les r\u00e8gles m\u00e9tier, et qui lib\u00e8rent le refactoring plut\u00f4t que de le freiner.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les sources sont sur GitHub : <a href=\"https:\/\/github.com\/dmissud\/black-box\">dmissud\/black-box<\/a> et <a href=\"https:\/\/github.com\/dmissud\/white-box\">dmissud\/white-box<\/a>. Les consignes sont dans <code>CONSIGNE.md<\/code> \u00e0 la racine de chaque repo \u2014 suffisant pour animer la session avec n&rsquo;importe quel groupe Java.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>Un dojo de code, c&rsquo;est une s\u00e9ance d&rsquo;entra\u00eenement d\u00e9lib\u00e9r\u00e9 : un probl\u00e8me bien d\u00e9fini, des contraintes explicites, et un d\u00e9brief qui fait r\u00e9fl\u00e9chir. Celui-ci tourne autour d&rsquo;un syst\u00e8me de biblioth\u00e8que et d&rsquo;une question fondamentale : quand on \u00e9crit des tests, est-ce qu&rsquo;on teste un comportement ou une impl\u00e9mentation ? Les repos sont disponibles sur GitHub [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[],"class_list":["post-36","post","type-post","status-publish","format-standard","hentry","category-craftsmanship"],"_links":{"self":[{"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/posts\/36","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/comments?post=36"}],"version-history":[{"count":1,"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/posts\/36\/revisions"}],"predecessor-version":[{"id":37,"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/posts\/36\/revisions\/37"}],"wp:attachment":[{"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/media?parent=36"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/categories?post=36"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/missud.eu\/index.php\/wp-json\/wp\/v2\/tags?post=36"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}