CodeGym /Cours /Spring Test /@AutoConfigureTestDatabase : replace = NONE

@AutoConfigureTestDatabase : replace = NONE

Spring Test
Niveau 16, Leçon 3
Disponible

1. La base de données embarquée ment parfois

Pendant que vous apprenez à écrire @DataJpaTest, la base intégrée ressemble à un partenaire idéal : elle démarre rapidement, vous n’avez rien à monter à la main, les tests filent comme un avion à réaction. Mais cette idylle a un effet secondaire : une base de données embarquée n’est pas un « petit PostgreSQL », mais une base complètement différente, avec son propre caractère, ses habitudes et sa compréhension des mots « unicité », « tri » et « date ».

Et c’est là que commence la situation classique de la vie : les tests sont verts, vous êtes content, puis l’application se connecte au vrai PostgreSQL – et soudain, il s’avère qu’une requête renvoie des résultats dans un ordre différent, que la migration se comporte autrement et qu’une erreur de contrainte apparaît là où vous ne l’attendiez pas. Ce n’est pas parce que Spring « s’est cassé ». C’est parce que vous avez testé le comportement sur un « simulateur », mais en réalité vous êtes monté sur un « ring vivant ».

La base de données embarquée est un outil honnête pour la rapidité et le retour d’information. L’important est de se rappeler : elle prouve la correction de la logique du repository en général, mais elle ne prouve pas toujours la correction du comportement spécifique de PostgreSQL, avec lequel votre ContentHub vit en production.

Il ne faut pas se tromper d’échelle ici : nous ne changeons pas le type de test et nous ne construisons pas maintenant une piste infrastructurelle séparée. C’est toujours le même @DataJpaTest pour le repository-layer ; seule change la question de savoir quelle base se trouve en dessous et quand l’environnement lui-même devient une partie du risque.

2. @DataJpaTest et la substitution du DataSource

@DataJpaTest : d’où vient la base de données

Pour gérer consciemment le choix de la base, il est utile de comprendre : @DataJpaTest est une tranche. Elle monte l’infrastructure JPA et les repositories, mais Spring Boot essaie de rendre le test aussi « léger » et autonome que possible. L’un des moyens est de remplacer automatiquement votre DataSource par une base embarquée, si elle est disponible dans le classpath.

Concrètement, cela se présente ainsi : dans votre application, dans application.yml, vous avez configuré PostgreSQL, mais dans les tests, si une dépendance vers une base embarquée (souvent H2) se trouve à côté, Boot dit : « Oh, on a une base in-memory, je vais la brancher pour que les tests ne dépendent pas d’un service externe. » Et il la branche.

Si on le représente sous forme de petit schéma, on obtient à peu près ceci :

flowchart TD
    A["@DataJpaTest démarrer"] --> B{Y a-t-il un driver embedded DB dans le classpath?}
    B -- "Oui" --> C["Boot remplace DataSource par la base embarquée"]
    B -- "Non" --> D["Boot utilise le DataSource de la configuration du test/projet"]
    C --> E["Les tests tournent vite, mais la base peut différer de PostgreSQL"]
    D --> F["Les tests sont plus proches de la prod, mais nécessitent une vraie base"]

Et c’est ici qu’apparaît l’idée clé de la leçon : parfois vous n’avez pas besoin d’une « base quelconque pour le test », mais précisément de votre base (par exemple PostgreSQL), parce que le risque de défaut réside dans les spécificités d’un dialecte, des types et du comportement.

@AutoConfigureTestDatabase : gestion de la substitution du DataSource

L’annotation @AutoConfigureTestDatabase ne concerne pas JPA, ni les repositories, ni un « super-mode de test ». C’est une chose très terre-à-terre : elle contrôle si Spring Boot essaiera de remplacer le DataSource par une base de données embarquée.

Point important : @DataJpaTest active déjà le comportement « remplacer la base si possible ». Par conséquent, lorsque vous ajoutez @AutoConfigureTestDatabase(replace = NONE), vous n’« activez pas une vraie base », vous interdisez la substitution.

@AutoConfigureTestDatabase a les modes Replace. Deux nous intéressent principalement : le défaut (substitution autorisée) et NONE (substitution interdite). Pour ne pas les retenir par cœur, il est pratique de les voir une fois dans un tableau :

Mode Replace Ce que fait Boot dans le test Interprétation simple
ANY Met une base embarquée si disponible Si on peut simplifier, on simplifie
NONE Ne remplace pas le DataSource Teste sur la base configurée

Concrètement, la transition ressemble à ceci : c’était (chemin embarqué par défaut), c’est devenu (chemin vraie base via configuration).

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // On monte seulement la tranche JPA : repositories et infrastructure, sans toute l'application
class ArticleRepositoryDataJpaTest {
    // Ici Boot peut remplacer DataSource par une base embarquée (si elle est dans le classpath)
}

Et la variante « ne pas remplacer » :

import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest // Toujours une tranche JPA, pas un @SpringBootTest complet
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // On interdit à Boot de remplacer DataSource par une base embarquée
class ArticleRepositoryRealDbTest {
    // Désormais le DataSource sera pris de votre configuration (par exemple du profil de test)
}

Notez un détail psychologique important : c’est toujours @DataJpaTest. Vous n’avez pas transformé le test en @SpringBootTest. Vous testez toujours la couche de persistance de manière étroite et relativement rapide. Vous avez simplement changé « quelle base se trouve en dessous ».

Ce n’est pas non plus un nouveau défaut pour toute la suite. La plupart des tests de repository vivent très bien sur le chemin embarqué et donnent un retour rapide ; replace = NONE est nécessaire là où le risque réside précisément dans le comportement d’une base spécifique.

3. replace = NONE en pratique

L’idée la plus utile ici est un peu ennuyeuse, mais très ingénieur : lors du passage à replace = NONE, la logique du test ne change généralement pas. Vous ne devez pas soudainement réécrire les assertions, ajouter de nouvelles vérifications ou modifier la structure AAA. Vous ne changez pas le test, mais l’environnement dans lequel il s’exécute.

Si le test vérifiait « le repository trouve un article publié par slug », il vérifie toujours la même chose. Simplement, vous n’obtenez plus la réponse de H2/HSQLDB/Derby, mais du vrai PostgreSQL (ou de la base qui est configurée comme vraie base du projet).

Voici un exemple dans le style ContentHub, le plus concret possible. Supposons que nous ayons une méthode qui lit un article par slug et statut :

import java.util.Optional;

import org.springframework.data.repository.Repository;

interface ArticleRepository extends Repository<Article, Long> {

    // On cherche un article par clé métier (slug) et statut de publication
    Optional<Article> findBySlugAndStatus(String slug, ArticleStatus status);
}

Et maintenant le test. Il a la même apparence pour le chemin embarqué et pour le chemin vraie base :

import org.junit.jupiter.api.Test; 
import org.springframework.beans.factory.annotation.Autowired;

import static org.assertj.core.api.Assertions.assertThat;

class ArticleRepositoryExampleTest {

    @Autowired
    ArticleRepository repository; // On teste le repository directement, sans service/contrôleur

    @Test
    void findsPublishedArticleBySlug() {
        // Action : on appelle la méthode du repository comme le ferait le code réel
        var result = repository.findBySlugAndStatus(
                "spring-boot-testing",
                ArticleStatus.PUBLISHED
        );

        // Vérification : pour l'exemple d'apprentissage, il suffit de s'assurer que l'article est trouvé
        assertThat(result).isPresent();
    }
}

Oui, en réalité vous devez préparer les données via TestEntityManager.persist(...), puis faire flush() et clear(). Mais le sens est ailleurs : le test lui-même ne devient pas « un autre test ». Il obtient simplement une preuve plus forte.

Et un autre point qui surprend souvent les débutants : les tests avec replace = NONE dans @DataJpaTest restent généralement transactionnels et font rollback. Cela signifie que vous n’« encrassez » pas la base avec des données infinies après chaque test – tant que vous n’avez pas de commit intentionnel ni de réglages spéciaux.

4. En quoi la base embarquée diffère de PostgreSQL

Quand on dit « une base de données embarquée peut mentir », cela ne signifie pas « elle renvoie accidentellement des données incorrectes ». Il s’agit généralement de certaines nuances limites qui se comportent différemment. Dans un projet réel, ce sont précisément ces nuances qui deviennent des régressions, car elles sont difficiles à voir à l’œil nu dans le code et faciles à manquer en relecture.

L’une des classes de divergences les plus fréquentes est le DDL et les contraintes. Vos migrations (Flyway) et le schéma PostgreSQL peuvent être plus riches : types, restrictions, index, nuances de longueur varchar, comportement des contraintes unique. La base embarquée pardonne parfois ce que PostgreSQL ne pardonnera pas, ou inversement : elle « râle » sur du DDL que PostgreSQL accepte tranquillement. Résultat : vous obtenez soit une fausse confiance, soit une fausse alerte.

La deuxième grande zone est les types date/heure et leur précision. PostgreSQL et les bases embarquées stockent les timestamps différemment, arrondissent les microsecondes/nanosecondes différemment, et traitent le fuseau horaire différemment. Si dans ContentHub l’ordre des publications dépend de publishedAt, vous pouvez attraper un bug très désagréable : le tri « fluctue » parce que dans une base le temps est stocké avec une précision, et dans une autre avec une précision différente. C’est particulièrement sournois lorsque vous créez beaucoup d’enregistrements « presque en même temps ».

La troisième zone est le tri, NULL et la stabilité de l’ordre. PostgreSQL a ses propres règles de tri des valeurs NULL (par exemple, dans certains modes, NULL peut être « le plus grand » et partir à la fin/au début selon la direction du tri). La base embarquée peut trier différemment. Si votre requête construit un flux public et que vous attendez un ordre stable, des différences dans NULLS FIRST/LAST ou dans la comparaison des chaînes peuvent n’apparaître que sur la vraie base.

La quatrième zone est la génération des ID et les séquences. JPA essaie de vous abstraire de la manière dont les clés sont générées, mais en pratique IDENTITY, SEQUENCE et « comment la base crée réellement les valeurs » influencent parfois l’ordre d’insertion, le moment où l’ID apparaît, et certains scénarios limites autour des opérations par lots. Cela « casse rarement tout », mais cela casse parfois des tests qui attendaient un ordre d’opérations et en ont obtenu un autre.

Enfin, la cinquième zone est le dialecte et les particularités SQL. JPQL est généralement portable, mais le SQL généré (en particulier pour la pagination, limit/offset, le tri, certaines fonctions) dépend du dialecte. Dans une base embarquée, vous pouvez avoir obtenu un SQL et un comportement, et dans PostgreSQL un autre. Si vous testez une requête importante (par exemple, l’affichage public des articles publiés), ces différences doivent justement être détectées le plus près possible de la réalité.

Important : il n’est pas nécessaire de retenir ce paragraphe comme une liste d’incantations. Une seule idée suffit : différentes bases ont des habitudes différentes, et replace = NONE permet de vérifier le comportement sur la base qui est importante pour votre projet.

5. Configuration minimale d’une vraie base

L’annotation @AutoConfigureTestDatabase(replace = NONE) elle-même ne crée pas PostgreSQL à partir de rien. Elle dit seulement à Boot : « ne remplace pas le datasource ». Vous devez donc comprendre d’où viendra le DataSource s’il n’y a plus de substitution embarquée.

Habituellement, dans un projet pédagogique, cela se résout via un profil de test, que vous avez déjà vu : application-test.yml dans src/test/resources ou src/main/resources (selon les règles du projet), et l’activation explicite du profil test. Nous ne discutons pas ici de la manière de monter PostgreSQL (Docker, Compose, etc.) – nous fixons simplement le principe : le test se connectera au datasource que vous avez configuré.

Un exemple conditionnel de src/test/resources/application-test.yml pour PostgreSQL pourrait ressembler à ceci :

spring:
  datasource:
    # URL JDBC de la base de test : les tests se connecteront ici
    url: jdbc:postgresql://localhost:5432/contenthub
    username: contenthub
    password: contenthub
  jpa:
    hibernate:
      # Dans les tests, il est utile de valider le schéma pour détecter les divergences avec les migrations
      ddl-auto: validate

Si vous utilisez des profils (et dans le cours nous les utilisons), le test aura souvent besoin de @ActiveProfiles("test") pour que ce YAML s’applique réellement :

import org.springframework.test.context.ActiveProfiles;

@ActiveProfiles("test") // On active le profil test pour prendre application-test.yml
class SomeDataTest {
    // Les annotations de la tranche de test ne sont pas montrées ici – seul le profil importe
}

Encore une fois : ce qui importe maintenant n’est pas de « tout configurer parfaitement », mais de comprendre que replace = NONE rend le test plus honnête, mais exige que vous ayez un environnement plus honnête. Sinon, le contexte pourrait ne tout simplement pas démarrer, car il n’a pas trouvé de datasource fonctionnel.

6. Erreurs typiques avec replace = NONE

Erreur n°1 : considérer replace = NONE comme « rends le test professionnel ».
Parfois on pense qu’il suffit d’ajouter une annotation et voilà, vous êtes un « développeur adulte ». En réalité, c’est juste un commutateur d’environnement. Si le test est faible en lui-même (par exemple, il vérifie seulement hasSize(2) sans vérifier la composition et l’ordre), sur PostgreSQL il restera tout aussi faible – seulement il s’exécutera plus lentement et échouera plus bruyamment.

Erreur n°2 : confondre « vraie base » et « contexte complet de l’application ».
@DataJpaTest avec replace = NONE reste une tranche de données. Vous ne testez pas les contrôleurs, les services et la sécurité. Si le test commence soudain à « exiger de manière inattendue » la moitié de l’application, c’est que vous avez accidentellement dépassé les limites de la couche (par exemple, importé une configuration superflue ou attiré des dépendances hors du monde de la persistance).

Erreur n°3 : modifier les assertions pour que le test « passe » sur la nouvelle base.
Une habitude très dangereuse : on passe à PostgreSQL, le test échoue à cause d’un comportement réel, et au lieu d’analyser, on « ajuste » l’attente. On peut ainsi légaliser un bug facilement. Si le résultat change, formulez d’abord pourquoi. Est-ce un défaut ? Une spécificité ? Un order-by incorrect ? Un problème avec NULL ? Ensuite seulement, modifiez le test ou le code.

Erreur n°4 : oublier que le datasource vient désormais de la configuration – et obtenir un échec du contexte.
Sur le chemin embarqué, beaucoup de choses fonctionnaient « prêtes à l’emploi » et vous pouviez ne pas vous demander d’où venait la base. Avec replace = NONE, le test essaie honnêtement de se connecter au vrai datasource. Si la configuration est absente ou la base indisponible, le test échouera dès le démarrage. C’est normal : l’outil n’est pas cassé, vous avez simplement demandé la réalité.

Erreur n°5 : basculer toutes les requêtes, même les plus simples, sur le chemin vraie base.
Même si vous voulez une « honnêteté maximale », le coût des tests est important. Une vraie base est généralement plus lente, exige plus de discipline d’environnement et peut dégrader la vitesse du retour d’information. Il est bien plus raisonnable de d’abord comprendre ce qui génère le risque, puis de décider quels tests doivent vraiment vivre avec replace = NONE. (Stop – nous n’allons pas plus loin pour ne pas anticiper le sujet du jour.)

1
Mission
Spring Test, niveau 16, leçon 3
Bloqué
Test de slice de données sans remplacement de DataSource
Test de slice de données sans remplacement de DataSource
1
Mission
Spring Test, niveau 16, leçon 3
Bloqué
Vérification de l'unicité du slug sur un chemin de base de données réelle
Vérification de l'unicité du slug sur un chemin de base de données réelle
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION