CodeGym /Cours /Spring Test /Quand le real PostgreSQL path est rentable

Quand le real PostgreSQL path est rentable

Spring Test
Niveau 25, Leçon 0
Disponible

1. Contexte : encore une fois à propos de la base de données

Honnêtement, tester une base de données rappelle les conversations sur l'exercice du matin. Tout le monde comprend que c'est utile, mais personne ne veut commencer par « l'artillerie lourde » le lundi à 7 heures du matin. Dans notre cours, c'est fait exprès : nous avons appris pendant de nombreux jours à obtenir de la confiance à moindre coût et rapidement, et aujourd'hui nous ajoutons une option coûteuse – exactement là où elle augmente réellement la confiance.

Le point clé : Testcontainers et le real PostgreSQL path ne sont pas une nouvelle base du cours, mais un amplificateur tardif. Nous ne réécrivons pas toute la suite de tests « sur des conteneurs », nous ne transformons pas le cours en cours Docker, et encore moins nous ne faisons « un conteneur à la place de l'architecture ». Nous ajoutons un outil qui n'est nécessaire qu'aux endroits où le comportement dépend spécifiquement de PostgreSQL, et non simplement « d'une base de données quelconque ».

Pour ne pas se perdre, fixons immédiatement deux idées. Premièrement : la plupart des tests de ContentHub (unitaires, JSON, tests MVC étroits) ne doivent pas du tout connaître PostgreSQL – car ils vérifient d'autres risques. Deuxièmement : certains scénarios de données et d'intégration sont vraiment sensibles au SGBD spécifique, aux migrations et au dialecte, et là, le « vrai » PostgreSQL cesse d'être un luxe.

Real PostgreSQL path et base embarquée

Quand on parle de « base de données embarquée », on fait le plus souvent référence à l'option « monter rapidement une base de données intégrée pour les tests » (par exemple H2), afin que @DataJpaTest fonctionne sans infrastructure externe. C'est vraiment pratique : les tests sont rapides, s'exécutent partout, ne nécessitent pas Docker, et un débutant peut travailler même sur un ordinateur portable qui n'a vu PostgreSQL que sur des images.

Le problème est que la base embarquée est un autre SGBD, avec un dialecte SQL différent, des contraintes différentes et parfois des « règles subtiles » différentes. Elle peut être excellente en tant que « terrain d'entraînement » et « filtre rapide de régressions », mais elle n'est pas obligée de coïncider avec PostgreSQL dans les détails. Or, dans le développement backend, ce sont précisément les détails qui aiment casser la production.

Le real PostgreSQL path est lorsque les tests sont exécutés contre PostgreSQL, et de préférence de manière à ce que le développeur n'ait pas à maintenir manuellement une base de données démarrée « quelque part ». Et c'est là qu'apparaît Testcontainers : une bibliothèque qui permet de démarrer un PostgreSQL temporaire dans un conteneur juste le temps des tests.

Important : la base réelle est un axe distinct, indépendant du niveau de test. Vous pouvez exécuter @DataJpaTest contre PostgreSQL, ou vous pouvez exécuter @SpringBootTest contre PostgreSQL. Ce sont différents niveaux de vérification, mais le même « chemin de base de données ».

3. Deux axes de choix : niveau de test et chemin vers la base de données

Il est très facile de faire une erreur et de « coller » ensemble deux décisions différentes : « monter le contexte complet » et « utiliser un vrai PostgreSQL ». En pratique, ce sont des boutons différents, et il faut les appuyer séparément, pas toujours les deux en même temps.

Après ce cours, vous devriez prendre l'habitude de penser en matrice : d'un côté — le niveau de test, de l'autre — le type de base.

Ce que nous choisissons Moins cher Plus cher Signification principale
Niveau de test @DataJpaTest / slices @SpringBootTest Vérifie seulement une couche ou vérifie le câblage entre les couches
Chemin vers la base de données embedded real PostgreSQL (conteneur) Vérifie « approximativement comme en production » le comportement du SGBD

Et voici pourquoi c'est important : si vous devez prouver que l'unicité de slug est réellement appliquée au niveau PostgreSQL, le plus souvent @DataJpaTest + PostgreSQL suffit. Monter en plus @SpringBootTest à ce moment-là, c'est comme aller chercher du pain en char : c'est spectaculaire, mais les voisins commenceront à poser des questions.

Pour fixer cela comme un schéma d'ingénierie, on peut représenter le flux de décision ainsi :

flowchart TD
    A[Besoin d'écrire un test] --> B{Le risque dépend-il de PostgreSQL?}
    B -- non --> C[On reste sur le chemin bon marché : unit / slice / embedded]
    B -- oui --> D{Peut-on prouver le risque au niveau data?}
    D -- oui --> E["@DataJpaTest + real PostgreSQL"]
    D -- non --> F["@SpringBootTest + real PostgreSQL selected"]

Aujourd'hui nous nous occupons principalement de la première question : comment comprendre que le risque dépend réellement de PostgreSQL et mérite un conteneur.

4. La base embarquée peut « mentir » : divergences typiques

Les jours précédents, nous avons déjà dit que le chemin embarqué peut parfois « mentir », mais maintenant nous devons transformer cela en intuition pratique. Pas en philosophie « toutes les bases sont différentes », mais en raisons concrètes : quelles sont les choses qui peuvent passer sur une base embarquée et échouer sur PostgreSQL (ou l'inverse). Et, plus important encore, lesquelles sont réellement pertinentes pour ContentHub.

Migrations Flyway et dialecte SQL

Si votre projet utilise Flyway, alors le schéma de la base de données n'est pas un « effet secondaire de JPA », mais un artefact séparé qui doit être exécuté sur le vrai SGBD. La base embarquée peut avoir une syntaxe différente, des types différents, des contraintes différentes – et vous pourriez accidentellement obtenir une situation où les migrations passent sur H2 mais échouent sur PostgreSQL, ou l'inverse.

Pour ContentHub, c'est particulièrement critique car nous avons les tables article, category, article_attachment, des contraintes, des index, éventuellement des séquences/identity, etc. Si une migration est « légèrement incorrecte », vous voulez le savoir grâce aux tests, pas à cause du message d'un collègue « pourquoi le staging ne démarre pas ».

Contraintes au niveau de la base de données et index

Nous avons déjà testé les contraintes unique et not-null. Mais il y a une subtilité : différentes bases se comportent différemment dans des détails comme les types, la longueur, le collationnement, le comportement avec null dans les index uniques, et même dans les messages d'erreur. Parfois, un « apparemment unique » sur une base embarquée se manifeste différemment que sur PostgreSQL.

Dans ContentHub, l'un des principaux invariants est l'unicité de slug. Formellement, c'est une simple contrainte unique. Mais ce sont précisément ces choses qui aiment se casser à cause du schéma, des migrations et d'un « index accidentellement différent ». Et c'est exactement le cas où un test sur un vrai PostgreSQL donne une bonne confiance : il vérifie non pas « comme nous pensons », mais « comme ce sera en réalité ».

Requêtes, tris et pagination

Les tests de tri sont un endroit où la base embarquée peut être sournoise. Elle peut trier les chaînes différemment, interpréter la casse différemment, traiter les null dans ORDER BY différemment, optimiser la requête différemment, et vous obtiendrez de beaux tests verts, alors que dans PostgreSQL « l'ordre est légèrement différent ».

Dans ContentHub, nous avons des scénarios comme « liste publique des articles publiés triés par publishedAt desc ». Il est important pour le client que l'ordre soit prévisible. Si vous voulez fixer un ordre spécifique et pas seulement le fait que « des articles sont retournés », alors la vérification sur un vrai PostgreSQL peut être justifiée.

Verrouillage, transactions et cas limites concurrents

Nous avons montré les bases de l'optimistic locking via version. Au niveau JPA, cela semble « identique », mais au niveau de la vraie base, il y a des nuances : isolation des transactions, comportement lors des mises à jour parallèles, exceptions précises que vous obtenez en cas de conflit.

Pour le cours, nous ne nous transformons pas en cours de concurrence. Mais si vous choisissez un ou deux tests qui démontrent un comportement sensible au verrouillage sur un vrai PostgreSQL, cela peut vous donner un sentiment très agréable : « J'ai vu cela en vrai, pas dans une présentation ».

5. Coût du chemin conteneurisé : pour quoi payez-vous

Si vous avez déjà entendu « vérifions tout avec des tests d'intégration », vous savez que cela se termine généralement par de la tristesse. Testcontainers est une chose utile, mais vous paierez avec des ressources bien réelles : le temps et la stabilité.

Premièrement, il faut démarrer les conteneurs. Même si l'image PostgreSQL est déjà téléchargée, le démarrage du conteneur et la disponibilité de la base prennent du temps. Par rapport à une base embarquée, c'est presque toujours plus lent, parfois nettement plus lent. Oui, cela peut être optimisé, mais aujourd'hui nous allons convenir d'une stratégie plus saine : ne pas optimiser un chemin infiniment coûteux – mais rendre le chemin coûteux petit.

Deuxièmement, les conteneurs ajoutent des points de défaillance supplémentaires. Un moteur Docker fonctionnel est nécessaire, des ressources sur la machine, parfois des problèmes de droits d'accès ou des limitations du portable d'entreprise. Ce n'est pas « horrible », c'est juste la réalité. Et c'est précisément pourquoi le chemin conteneurisé est tardif et ponctuel.

Troisièmement, les tests coûteux ont tendance à « s'alourdir de sens ». Du moment que vous avez monté PostgreSQL, la tentation d'ajouter un autre test apparaît… puis un autre… puis « et si on mettait toute la suite data ici »… et vous transformez imperceptiblement un ensemble de tests rapides en un monstre lent que personne n'aime exécuter localement.

C'est pourquoi notre objectif du jour 25 est presque cynique : obtenir un maximum de confiance avec un minimum de douleur d'infrastructure. Et cela signifie automatiquement : il faut un sous-ensemble basé sur des conteneurs, petit et explicable.

6. Quand le real PostgreSQL path est rentable

Nous faisons maintenant la chose la plus importante de cette leçon : transformer « apparemment utile » en un filtrage compréhensible. Imaginez que vous êtes dans une équipe et que quelqu'un propose : « Faisons tout tourner avec Testcontainers ». Vous devez être capable de répondre calmement : « D'accord, quel risque attrapons-nous, et pourquoi ne peut-il pas être attrapé moins cher ? ».

Un bon critère de rentabilité sonne presque toujours ainsi : nous soupçonnons que le comportement dépend du SGBD spécifique (PostgreSQL) et nous voulons le fixer par un test.

En pratique, cela se traduit par plusieurs types de situations.

Le premier type — migrations et dérive de schéma. Si votre test doit prouver « les migrations sont effectivement appliquées sur PostgreSQL », alors une base embarquée ne convient pas par définition. Ici, Testcontainers est très pertinent car vous vérifiez précisément la vérité PostgreSQL : type SQL, index, contrainte — tout « comme dans la vraie vie ».

Le deuxième type — invariants et contraintes au niveau de la base. Contrainte unique sur slug, champs not-null, contraintes de longueur, clés étrangères — tout ce qui doit protéger votre domaine même si le code s'est trompé. C'est la zone de responsabilité de la base, et la vérifier sur « une autre base » est parfois normal, mais parfois risqué.

Le troisième type — requêtes sensibles au dialecte et tri. Si vous écrivez une requête qui utilise des fonctionnalités spécifiques à PostgreSQL (ou se comporte simplement subtilement sur différents SGBD), et que cette requête est vraiment critique pour l'API, alors un seul test sur un vrai PostgreSQL peut être plus rentable qu'une semaine à chercher « pourquoi sur le staging c'est différent ».

Le quatrième type — cas limites de verrouillage/concurrence. Encore une fois, sans fanatisme : 1–2 scénarios pour démontrer et protéger un comportement vraiment important.

Et séparément, il faut être capable de dire honnêtement « non » au conteneur quand il n'apporte pas de bénéfice. Si le risque ne dépend pas de PostgreSQL, alors PostgreSQL dans le test n'est qu'une décoration coûteuse.

7. Quand PostgreSQL n'apporte presque rien

Il est très important de ne pas transformer Testcontainers en « version premium des tests ». Il ne rend pas un test automatiquement plus intelligent. Il le rend plus proche de la réalité de la base, et c'est tout. Si le risque n'est pas dans la base — le conteneur ne guérit pas.

Les tests unitaires des règles métier (PublicationPolicy, vérifications des transitions de statut) ne deviennent pas plus fiables avec PostgreSQL, car ils ne concernent pas la base. C'est de la « logique pure », et la meilleure façon de la rendre fiable est de garder le test simple et rapide.

Les tests JSON ne deviennent pas plus fiables avec PostgreSQL, car la sérialisation des DTO et le contrat ApiProblem ne dépendent pas du tout du SGBD.

Les tests MVC étroits (@WebMvcTest) ne doivent pas traîner PostgreSQL derrière eux s'ils vérifient le contrat HTTP et le comportement du controller/advice/validation, et non les subtilités de SQL.

Même les tests de sécurité, dans de nombreux cas, n'ont pas besoin de PostgreSQL. Oui, la sécurité peut dépendre des utilisateurs dans la base dans un projet réel, mais dans notre cours, la baseline est plus simple, et la majeure partie de la logique de sécurité est vérifiée au niveau des accès et des statuts, et non du dialecte de la base.

Cette partie est importante psychologiquement : vous n'êtes pas un « mauvais testeur » si vous avez peu de tests conteneurisés. Au contraire, si vous pouvez expliquer un par un chaque test conteneurisé, c'est souvent un signe de maturité.

8. Exemples de candidats pour le chemin base réelle

Nous allons maintenant faire quelques mini-exemples pour que votre cerveau associe la discussion sur les risques à des classes de tests concrètes. Les exemples sont petits et délibérément un peu « squelettiques » : ici, ce n'est pas le câblage qui est important, mais la capacité à reconnaître où PostgreSQL a du sens.

Dans les exemples ci-dessous, nous utilisons JUnit 6.

Test unitaire PublicationPolicy : PostgreSQL n'a rien à voir ici

Dans ce test, nous vérifions une règle métier pure : peut-on passer un article de DRAFT à IN_REVIEW. La base de données n'est pas du tout impliquée, et c'est parfait : le test est rapide et ne casse pas à cause de l'infrastructure.


import org.junit.jupiter.api.Test;

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

class PublicationPolicyTest {

    @Test
    void allowsDraftToReview() {
        // Création de l'objet de politique domaine : c'est de la logique pure, sans base de données ni Spring
        var policy = new PublicationPolicy();

        // Vérifie la règle spécifique de transition de statut
        assertThat(policy.canMove(ArticleStatus.DRAFT, ArticleStatus.IN_REVIEW)).isTrue();
    }
}

Si un tel test est « renforcé » avec PostgreSQL, vous n'attraperez pas plus de défauts — vous attendrez simplement plus longtemps le résultat.

@DataJpaTest ordinaire : utile sans conteneur

Voici un test data‑slice typique sur un repository. Il peut être très précieux même sans conteneur PostgreSQL : vérifier une requête, les relations, la sémantique de base. Nous connectons un conteneur seulement si un risque sensible à la base est présent.


import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

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

@DataJpaTest // On ne monte que la couche JPA (référentiels/EntityManager), sans le contexte complet
class ArticleRepositoryDataJpaTest {

    @Autowired
    ArticleRepository articleRepository; // Spring injecte le référentiel testé

    @Test
    void findsPublishedBySlug() {
        // Dans un vrai test, il y a généralement une préparation de données (fixture) avant la recherche

        // Vérifie que le repository sait trouver un article publié par slug
        assertThat(articleRepository.findPublishedBySlug("spring-boot-testing")).isPresent();
    }
}

En soi, une telle vérification est bonne. Mais la déplacer dans un conteneur « par habitude » n'est pas une bonne idée.

« Signal » que le test est sensible à la base : replace = NONE

Lorsque vous voyez dans un test @AutoConfigureTestDatabase(replace = NONE), cela signifie presque toujours que l'auteur du test a consciemment décidé de « ne pas remplacer la source de données ». Autrement dit, le test veut travailler avec la source de données que nous considérons comme « réelle » (dans notre cas, PostgreSQL dans un conteneur).


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

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // Ne remplace pas la source de données par une base embarquée
class ArticleRepositoryPostgresContainerTest {

    // Ici nous fixons l'intention : cette classe de test fonctionnera avec une vraie base de données (PostgreSQL dans un conteneur)
}

Cette classe est encore vide — et c'est normal : ici nous fixons seulement l'intention. Pour cet endroit, il est important de voir le choix lui-même — le test va consciemment dans le chemin base réelle, et les vérifications spécifiques seront construites autour des contraintes, du tri ou du verrouillage.

Comment repérer les candidats dans ContentHub

Il est maintenant utile de relier les critères de rentabilité à nos entités et scénarios concrets. ContentHub n'est pas une base de données abstraite, mais des risques tout à fait concrets.

Scénario dans ContentHub Pourquoi c'est sensible à la base de données Quel type de test est généralement suffisant
Unicité de Article.slug C'est un invariant au niveau du schéma/index, une erreur ici affecte les liens publics @DataJpaTest + real PostgreSQL
Migrations Flyway pour les tables et index Le SQL doit être valide spécifiquement pour PostgreSQL 1 test conteneurisé qui monte le contexte et exécute les migrations
Tri de la liste publique (publishedAt desc) Les tris et les cas limites de null peuvent se comporter différemment sur différents SGBD @DataJpaTest + PostgreSQL pour la requête choisie
Optimistic locking (version) Les nuances du comportement concurrent sont mieux vues sur une base réelle 1 test démonstratif @DataJpaTest/test d'intégration

Notez qu'il n'y a pas ici de « vérifier la sérialisation DTO » ou « vérifier que le contrôleur renvoie 200 ». Ce n'est pas parce qu'ils ne sont pas importants — ils le sont, mais PostgreSQL n'y ajoute rien.

9. Erreurs typiques lors du choix du real PostgreSQL path

Erreur n°1 : « Puisque nous le pouvons, transférons tous les tests data sur des conteneurs. »
Cela semble logique, mais tue presque toujours la vitesse de feedback. En conséquence, les développeurs arrêtent d'exécuter les tests localement, et la suite devient « quelque chose pour le CI ». Il est plus correct de garder le chemin conteneurisé petit : seulement les scénarios sensibles à la base qui sont réellement rentables.

Erreur n°2 : Essayer de renforcer les tests unitaires avec un conteneur.
Les tests unitaires sont précieux parce qu'ils sont rapides et isolés. Si vous commencez à « monter Postgres pour PublicationPolicy », vous n'ajoutez pas de confiance — vous compliquez simplement l'exécution. Une règle métier ne deviendra pas plus vraie parce qu'une base de données tourne à côté.

Erreur n°3 : Choisir un CRUD trivial comme principal candidat pour le chemin conteneurisé.
Sauvegarder et lire une entité est utile, mais s'il n'y a pas de risque sensible à la base, le conteneur n'est généralement pas rentable. Il est bien plus rentable de choisir un test qui attrape réellement une divergence : migration, index, tri, requête dialectale, cas limite de verrouillage.

Erreur n°4 : Mélanger « base réelle » et « contexte complet » comme une paire obligatoire.
Parfois, les gens pensent : « Si Postgres, alors @SpringBootTest ». Non. Si le risque réside dans le repository et le schéma, @DataJpaTest contre PostgreSQL donnera une vérification forte beaucoup moins chère qu'un contexte complet.

Erreur n°5 : Le conteneur est connecté, mais la vérification significative est oubliée.
Il arrive qu'un test soit « conteneurisé » du point de vue de l'infrastructure, mais ne vérifie en substance que status().isOk() ou « la méthode n'a pas renvoyé null ». Le conteneur lui-même ne prouve rien. Il n'a de sens qu'avec la vérification de l'effet réel : violation de contrainte, migration, tri, relecture depuis la base après écriture.

1
Mission
Spring Test, niveau 25, leçon 0
Bloqué
Test unitaire local sans Spring et Docker
Test unitaire local sans Spring et Docker
1
Mission
Spring Test, niveau 25, leçon 0
Bloqué
Test rapide du repository pour un scénario avec ordre
Test rapide du repository pour un scénario avec ordre
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION