CodeGym /Cours /Spring Test /Architecture des tests MVC par zones

Architecture des tests MVC par zones

Spring Test
Niveau 13, Leçon 3
Disponible

1. Architecture des tests MVC

Lorsque les tests MVC deviennent plus nombreux, le problème n’est généralement pas le « manque d’annotations », mais le fait que les tests commencent à vivre de manière chaotique : des morceaux de public API se mélangent avec editor, des scénarios attachment apparaissent soudainement, et au bout d’une semaine personne ne se souvient pourquoi le mock d’un service inutile pour la moitié des tests traîne dans la classe de base. L’architecture du suite est un moyen de rendre les tests prévisibles et rapides à maintenir, afin que n’importe qui (y compris vous dans un mois) puisse trouver le scénario nécessaire en une minute, et non en une demi‑journée de fouilles archéologiques.

Dans un projet réel, les tests MVC croissent souvent « au besoin » : on ajoute un endpoint, on ajoute un test. C’est normal. Ce qui n’est pas normal, c’est que chaque nouveau test soit ajouté dans un fichier aléatoire et que les éléments communs soient copiés à dix endroits. À un moment donné, vous vous retrouvez non pas avec un suite, mais avec une « décharge de tests », où chaque test est vert en apparence, mais toute modification fait peur : la moindre correction casse la moitié du monde, parce que les tests sont trop liés.

C’est pourquoi, à ce stade du cours, nous fixons une idée simple : la suite MVC doit refléter les surfaces réelles de l’API. Dans ContentHub, ces surfaces sont déjà explicitement séparées par des préfixes URI (/api/public/**, /api/editor/**, /api/admin/**) et par le sens des scénarios. Par conséquent, la structure des tests doit reproduire cette image, et non la contredire.

2. Zones API : public, editor, admin

Si l’on regarde l’API « avec les yeux du client », public, editor et admin ne sont pas simplement des contrôleurs différents. Ce sont des attentes différentes vis‑à‑vis du contrat : l’API publique est presque toujours liée à la lecture et aux paramètres de recherche, l’API editor concerne la création/modification et les erreurs applicatives de l’utilisateur, l’API admin traite des actions administratives et des conflits métier sur les statuts. Lorsque nous mélangeons tout cela dans un même chaudron de tests, nous perdons les frontières sémantiques et nous commençons à écrire des vérifications « au cas où », et non en fonction du risque.

Pour que cette discussion soit concrète, il est pratique d’avoir en tête un petit tableau. Il aide à ne pas perdre de vue « où nous sommes en ce moment » lors de la lecture d’un test et quelles assertions sont généralement les plus importantes dans cette zone.

Zone Préfixe URI Opérations typiques Ce qui est généralement important dans les tests MVC
public /api/public/... lecture des articles publiés pagination/tri/filtres,
200/404
, format JSON de la réponse
editor /api/editor/... création/édition, soumission, pièces jointes 201/200/400/409, validation, multipart, en‑têtes de téléchargement, contrat d’erreur
admin /api/admin/... approbation/rejet/archivage, liste admin 200/409/404, statuts corrects et payload d’erreur

Remarquez que nous ne discutons pas ici des « droits d’accès » en tant qu’objet de vérification. Dans une application réelle, les zones public/editor/admin sont presque toujours liées à la sécurité, mais au niveau de l’architecture de la suite MVC, il est d’abord important de séparer les surfaces : sinon, les vérifications d’accès se mélangent avec les scénarios HTTP courants et la suite perd rapidement sa forme.

Autre nuance pratique : chaque zone possède généralement un ensemble différent de helpers « préférés ». Public nécessite le plus souvent des utilitaires pratiques pour les paramètres de requête (page, size, sort, category), editor des utilitaires pour le multipart et les fichiers de test, admin des squelettes pratiques pour des commandes comme approve/reject. Si tout est mélangé, les helpers commencent à grossir « pour tout », se transformant en mini‑framework qui finit par masquer la sémantique HTTP et rendre la lecture du test difficile.

3. Règle : un contrôleur, un test

Lorsque nous sommes tentés d’écrire « un seul grand test pour toute l’API », cela semble rationnel : « moins de fichiers, plus simple à trouver ». En pratique, c’est l’inverse qui se produit. Une grande classe de test se transforme très vite en dépotoir de contextes : vous devez mocker trop de dépendances car il y a beaucoup de contrôleurs ; vous avez des @BeforeEach avec une préparation inutile pour la moitié des tests ; et le risque de casser accidentellement un autre test augmente parce que vous avez modifié une configuration commune pour un seul cas.

La règle « un contrôleur, une classe de test » fonctionne comme une norme d’hygiène. Elle vous oblige à garder un @WebMvcTest étroit, donc rapide et compréhensible. Vous ouvrez le fichier PublicArticleControllerWebMvcTest et votre cerveau comprend instantanément que seuls les scénarios GET /api/public/articles et GET /api/public/articles/{slug} y vivent. Pas d’upload, pas d’admin approve. C’est simplement agréable.

Cela dit, « un contrôleur, une classe » n’interdit pas d’avoir plusieurs scénarios au sein de la classe. Au contraire : à l’intérieur d’un même contrôleur, vous pouvez organiser les tests comme vous le souhaitez. Les classes @Nested fonctionnent souvent bien : par exemple, un groupe distinct pour la pagination, un autre pour le tri, un autre pour les paramètres négatifs. Mais l’essentiel est que la frontière « contrôleur → classe de test » reste claire.

Vous trouverez ci‑dessous un squelette minimal qui illustre ce principe sans se transformer en préparation de 200 lignes.

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;

// MVC-slice : nous levons un contexte étroit autour d’un seul contrôleur.
@WebMvcTest(PublicArticleController.class)
class PublicArticleControllerWebMvcTest {

    @Test
    void shouldReturnPublishedArticles() {
        // Les vérifications du contrat de réponse (format JSON, pagination, etc.) apparaîtront plus tard.
    }
}

Pour l’instant, cela semble trop vide, mais c’est justement le but : nous construisons l’architecture comme un « squelette » qui sera ensuite recouvert de muscles. Si le squelette est tordu, les muscles aussi auront un aspect… particulier.

4. Structure des tests dans src/test/java

La structure des tests n’est pas « la beauté pour la beauté ». C’est un moyen de faire travailler l’IDE et le cerveau ensemble. Lorsque vous voyez un package ...api.controller.editor, vous vous attendez immédiatement à des endpoints editor et à leur spécificité. Lorsque vous voyez ...api.controller.publicapi, vous ne vous attendez pas à trouver MockMultipartFile. Cela réduit la charge cognitive plus qu’il n’y paraît : le cerveau cesse de tout garder en tête, car la structure suggère le contexte.

La démarche la plus simple et la plus efficace dans ContentHub est de refléter la structure des packages de production au niveau des tests, mais en ajoutant des « zones ». Si vos contrôleurs se trouvent dans ...api.controller, il est logique de créer des sous‑packages publicapi, editor, admin pour les tests. Le préfixe URI reste /api/public/**, mais il est préférable de ne pas utiliser « public » comme nom de package Java (mot clé), donc un alias compatible avec la compilation.

src/test/java
└── com.example.contenthub
    └── api
        └── controller
            ├── publicapi
            │   ├── PublicArticleControllerWebMvcTest.java
            │   └── PublicArticleUrls.java
            ├── editor
            │   ├── EditorArticleControllerWebMvcTest.java
            │   ├── EditorArticleUrls.java
            │   └── AttachmentTestFiles.java
            └── admin
                ├── AdminArticleControllerWebMvcTest.java
                └── AdminArticleUrls.java

Remarquez : les helpers se trouvent à côté des tests de leur zone. Ce n’est pas un dogme, mais c’est souvent très pratique. Lorsque vous placez EditorArticleUrls à côté de EditorArticleControllerWebMvcTest, vous ne créez pas de package « utilitaire » global dans lequel on finit par entasser toutes sortes de choses, y compris des constantes « magiques » et des bouts de logique.

Si vous avez tout de même besoin d’un helper commun (par exemple, un chargeur de fixtures JSON ou un petit utilitaire pour les assertions ApiProblem), il est préférable de le garder dans un package de test séparé, mais avec une responsabilité très stricte. Par exemple, com.example.contenthub.testutil.json ou ...testutil.web. L’important est que cela ne devienne pas un « dépotoir général », mais reste un ensemble de petits outils honnêtes.

Autre détail pratique : src/test/resources doit aussi être structuré. Si vous avez des fichiers de test pour le multipart (image, petit binaire), ne les placez pas à la racine des ressources. Créez quelque chose comme src/test/resources/files/attachments/cover.png. Ainsi, le dépôt ne donnera pas l’impression que « quelqu’un a explosé le dossier des ressources et que nous vivons désormais parmi les débris ».

5. Helpers et fixtures

Lorsque les gens entendent « architecture des tests », ils imaginent souvent « créer une classe de base commune pour tous les tests ». Cela semble tentant : on configure une fois et c’est prêt. Mais dans le contexte MVC‑slice, cela conduit presque toujours à l’inverse : la base commune commence à tirer des mocks inutiles, des @Import superflus, des configurations de sérialisation inutiles, et au final vous obtenez plus de contextes, moins de transparence et plus de plantages mystérieux.

Il est bien plus sain de penser ainsi : un helper partagé est ce qui réduit le bruit sans changer le sens. Par exemple, extraire les chaînes d’URL dans une classe séparée est normal : vous éliminez la duplication sans masquer le HTTP. En revanche, extraire « faire une requête vers un endpoint editor avec les bons en‑têtes, le bon corps, les paramètres par défaut et les vérifications » est dangereux, car vous commencez à cacher le contrat dans un DSL.

Petit exemple de helper « sûr » pour les URL. Il ne fait pas le malin, il construit simplement une chaîne, et le test indique toujours explicitement les paramètres.

final class PublicArticleUrls {

    private PublicArticleUrls() {
        // Classe utilitaire : les instances ne sont pas nécessaires.
    }

    static String list() {
        // On fixe explicitement le endpoint public pour la liste, sans « magie » ni DSL.
        return "/api/public/articles";
    }

    static String details(String slug) {
        // slug fait partie du chemin, pas d’un paramètre de requête : plus facile à lire dans les tests.
        return "/api/public/articles/" + slug;
    }
}

Une approche similaire peut être adoptée pour les zones editor/admin, mais il est important de ne pas les mélanger. L’idée est simple : PublicArticleUrls ne doit rien savoir des pièces jointes, et EditorArticleUrls ne doit rien savoir du tri de la liste publique. Si un helper commence à « en savoir trop », il cesse d’être un helper pour devenir une API alternative qu’il faut maintenir.

Même logique pour les fixtures. Pour l’upload/download editor, il est pratique d’avoir une petite classe qui crée un MockMultipartFile avec des paramètres prévisibles. Mais qu’elle soit locale au package editor, afin que les tests public ne commencent pas « par hasard » à trimballer des scénarios de fichiers.

import org.springframework.mock.web.MockMultipartFile;

final class AttachmentTestFiles {

    private AttachmentTestFiles() {
        // Classe utilitaire : fabrique de fichiers de test.
    }

    static MockMultipartFile smallPng() {
        // Fichier « pseudo » minimal : le nom du champ, le filename et le content-type sont importants.
        return new MockMultipartFile(
                "file",
                "cover.png",
                "image/png",
                new byte[] { 1, 2, 3 } // Le contenu n’est pas important si nous ne testons pas le traitement réel de l’image.
        );
    }
}

C’est court, clair et ne transforme pas les tests en « théâtre magique ».

6. Configuration de @WebMvcTest par zone

La raison la plus fréquente pour laquelle un MVC‑slice « devient soudainement lent et étrange » est la croissance incontrôlée de la configuration. À un moment donné, quelqu’un ajoute un @Import sur la moitié du projet, puis un autre, puis une classe de base commune qui importe tout « au cas où ». Résultat : le @WebMvcTest se transforme en un contexte presque complet, seulement sans les repositories, mais avec le même niveau de douleur.

L’intuition correcte ici est la suivante : chaque zone possède son propre ensemble minimal d’infrastructure. Dans les tests public, vous avez généralement besoin du contrôleur, de son service (mocké), d’un @ControllerAdvice pour les erreurs et, éventuellement, de la configuration JSON/mapper d’objet (qui vient de toute façon avec l’auto‑configuration Boot). Dans les tests editor, la partie multipart s’ajoute, mais le contrôleur et ses dépendances restent la base. Dans les tests admin, le plus souvent rien de « spécial » n’est nécessaire – à part les mêmes éléments de base.

Exemple d’un slice MVC public soigné avec un exception handler explicitement ajouté et un service mocké. On voit ici que le test s’articule autour du contrôleur, et non autour d’une demi‑application.

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@WebMvcTest(PublicArticleController.class)
// On ajoute le gestionnaire d’erreurs commun pour que le contrat d’erreur soit également stable dans les tests.
@Import(ApiExceptionHandler.class)
class PublicArticleControllerWebMvcTest {

    @MockitoBean
    private PublicArticleService publicArticleService; // On mocke la dépendance du contrôleur sans lever la couche service.
}

La suite Editor se présente de manière similaire, avec des mocks différents. L’essentiel est que les dépendances reflètent le contrôleur réel : si le contrôleur editor travaille avec des opérations de pièces jointes via un service séparé, mockez‑le. S’il délègue tout à une seule façade, mockez la façade. Mais n’ajoutez pas des services étrangers « au cas où », sinon vous commencerez très vite à tester non pas le contrat, mais vos mocks.

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@WebMvcTest(EditorArticleController.class)
@Import(ApiExceptionHandler.class) // Pour la zone editor, il est particulièrement important de tester le contrat d’erreur (validation, 409, etc.).
class EditorArticleControllerWebMvcTest {

    @MockitoBean
    private EditorArticleService editorArticleService; // Exactement les dépendances nécessaires à ce contrôleur.
}

La suite Admin – une classe séparée. Même s’il n’y a « que deux tests », une classe séparée est tout de même meilleure. Elle deviendra le lieu naturel où viendront s’ajouter les futures vérifications des actions administratives, et vous n’aurez pas à déplacer les tests plus tard, comme des habitants en cours de rénovation.

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@WebMvcTest(AdminArticleController.class)
@Import(ApiExceptionHandler.class) // Les conflits de statut et les erreurs métier doivent aussi avoir un format uniforme.
class AdminArticleControllerWebMvcTest {

    @MockitoBean
    private AdminArticleService adminArticleService; // Les actions admin sont souvent liées à des commandes service.
}

Et encore une nuance concernant les propriétés. Parfois, les zones diffèrent par leurs limites et leur configuration (par exemple, la limite de taille des pièces jointes). Si vous devez redéfinir une propriété pour une classe de test particulière, faites‑le localement pour ne pas contaminer les autres suites. Ici, il est important de respecter le principe : la configuration fait aussi partie du contrat de la zone, mais ne transformez pas les redéfinitions de propriétés en une bouillie globale.

Mini‑template pour les trois suites

Une fois la structure des packages et la configuration fixées, il devient plus facile d’écrire les tests : vous savez déjà où ajouter un nouveau cas et quel style y est attendu. Idéalement, tout nouveau test « atterrit » naturellement dans le bon package, et vous n’avez pas à réfléchir à l’endroit où il va vivre. C’est comme des tiroirs bien rangés dans une cuisine : les couteaux ne sont pas rangés dans le réfrigérateur, même si « c’est plus proche ».

Ci‑dessous, un court exemple de ce à quoi pourraient ressembler les squelettes de tests par zone. Je montre délibérément un petit test par zone pour que l’on voie l’idée « surfaces différentes → scénarios différents », et non pas pour que nous réécrivions la moitié du projet.

Suite public : accent sur les paramètres de requête et les métadonnées de page.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

class PublicArticleControllerWebMvcTest {

    @Autowired
    private MockMvcTester mvc; // On teste la frontière HTTP, donc on travaille avec MockMvc.

    @Test
    void shouldReturnDefaultPageWhenNoParams() {
        // Public : scénario typique — requête sans paramètres et « page par défaut » de base.
        mvc.perform(get(PublicArticleUrls.list()))
                .assertThat()
                .hasStatusOk(); // Vérification minimale : statut. Les vérifications JSON seront ajoutées dans les tests spécialisés.
    }
}

Suite editor : accent sur le multipart et le contrat du champ file. On voit très bien que c’est un monde différent de la liste publique.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;

class EditorArticleControllerWebMvcTest {

    @Autowired
    private MockMvcTester mvc; // Ici, il est important de voir le multipart comme une véritable requête HTTP.

    @Test
    void shouldUploadAttachment() {
        // Editor : multipart fait partie du contrat, donc cela doit être explicite dans le test.
        mvc.perform(multipart("/api/editor/articles/{id}/attachments", 10L)
                        .file(AttachmentTestFiles.smallPng())) // Le champ « file » et le content-type doivent correspondre au contrat.
                .assertThat()
                .hasStatusOk();
    }
}

Suite admin : accent sur l’action administrative. Oui, le test a l’air simple – et c’est normal. L’architecture de la suite n’a pas besoin d’être complexe pour être utile.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

class AdminArticleControllerWebMvcTest {

    @Autowired
    private MockMvcTester mvc; // Les scénarios admin sont souvent « commandes » : POST sur une action.

    @Test
    void shouldApproveArticle() {
        // Admin : il est important de tester le endpoint de l’action et le statut attendu (souvent 200 ou 409).
        mvc.perform(post("/api/admin/articles/{id}/approve", 10L))
                .assertThat()
                .hasStatusOk();
    }
}

Ici, quelqu’un pourrait demander : « Et les vérifications JSON, les en‑têtes, et tout le reste ? » Elles viendront – et vous avez déjà les cours et les outils pour les écrire. Mais le but de cette leçon est différent : montrer que ces vérifications doivent vivre aux bons endroits. Ainsi, la suite croît non pas comme une boule de neige chaotique, mais comme une bibliothèque soignée de scénarios par zone.

7. Erreurs typiques dans l’architecture d’une suite de tests MVC

Erreur n°1 : une seule classe de test énorme pour toute l’API.
Au début, cela semble pratique : un fichier, un contexte, « tout à portée de main ». Ensuite, ce fichier contient 40 tests, 12 mocks, une préparation commune dans @BeforeEach que personne ne comprend, et tout nouveau scénario casse quelque chose à un endroit inattendu. La suite MVC supporte mieux la croissance lorsqu’elle est divisée par surfaces.

Erreur n°2 : une classe de base commune qui « aide tout le monde » mais en fait gêne.
Une classe de base commence presque toujours à tirer une configuration superflue, des @Import superflus et des mocks superflus. En conséquence, le @WebMvcTest cesse d’être un test slice étroit. Si vous voulez vraiment utiliser la réutilisabilité, commencez par de petites classes final de helpers et des utilitaires locaux à côté des tests, plutôt que par l’héritage.

Erreur n°3 : un package utilitaire commun où l’on entasse tout.
Au début, on y met les Urls, puis les fabriques de DTO, puis une méthode « pratique » qui effectue une requête et vérifie immédiatement la moitié de la réponse, puis des constantes de statuts, et finalement le package util devient une application alternative. Un helper utile doit soit être petit et honnête, soit être local à sa zone d’API.

Erreur n°4 : les helpers masquent le contrat HTTP au lieu de réduire le bruit.
Il y a une frontière fine entre « supprimer une chaîne d’URL dupliquée » et « cacher le sens du test ». Si le test se lisait comme un scénario HTTP et qu’après remaniement il se lit comme un appel à un DSL inconnu, vous avez gagné contre la copie‑collé, mais perdu en compréhension. Dans les tests MVC, c’est particulièrement douloureux : vous testez précisément la frontière HTTP, et elle doit être visible.

Erreur n°5 : mélange des zones en réutilisant des éléments « pratiques ».
Par exemple, vous avez créé un helper pour les requêtes editor (avec multipart, en‑têtes, quelques valeurs par défaut) et vous avez commencé à l’utiliser dans les tests public « parce que c’est pratique ». Au bout d’un moment, la suite public dépend inopinément des détails de l’API editor. Il est correct de garder les helpers et les fixtures « dans leur zone », sinon vous obtiendrez un couplage de test là où il n’existe pas dans le produit.

1
Mission
Spring Test, niveau 13, leçon 3
Bloqué
Trois tests MVC-slice étroits par zones API
Trois tests MVC-slice étroits par zones API
1
Mission
Spring Test, niveau 13, leçon 3
Bloqué
Classes helper locales par zones API
Classes helper locales par zones API
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION