Contactez-nous

Tests de la couche persistance (`@DataJpaTest`, `@DataJdbcTest`, `@DataMongoTest`)

Maîtrisez les tests de la couche persistance dans Spring Boot en utilisant les annotations de slice test @DataJpaTest, @DataJdbcTest et @DataMongoTest.

Introduction aux tests de la couche de persistance (slice tests)

Tester la couche de persistance est fondamental pour s'assurer que vos entités sont correctement mappées, que vos requêtes fonctionnent comme prévu et que les interactions avec la base de données sont fiables. Spring Boot propose des annotations spécifiques, appelées "slice tests", pour tester cette couche de manière isolée et efficace : `@DataJpaTest`, `@DataJdbcTest`, et `@DataMongoTest`.

L'idée derrière ces annotations est de ne charger qu'une partie ("slice") du contexte d'application Spring, spécifiquement celle nécessaire pour tester la persistance des données. Cela inclut la configuration de la source de données (souvent une base de données en mémoire par défaut), la découverte des entités ou documents, et l'instanciation des beans de repositories correspondants (JPA, JDBC, ou MongoDB).

Utiliser ces slice tests présente plusieurs avantages par rapport à un test d'intégration complet avec `@SpringBootTest` :

  • Rapidité : Charger uniquement la couche de persistance est beaucoup plus rapide que de démarrer l'ensemble de l'application.
  • Isolation : Vous vous concentrez uniquement sur la logique de persistance, sans interférences des couches service ou web.
  • Configuration simplifiée : Ces annotations fournissent des configurations par défaut adaptées aux tests de persistance (base en mémoire, transactions, etc.).

Nous allons explorer chacune de ces annotations en détail.

@DataJpaTest : tester vos repositories JPA

L'annotation `@DataJpaTest` est conçue spécifiquement pour tester les composants liés à JPA (Java Persistence API). Lorsque vous l'utilisez sur une classe de test, elle effectue plusieurs actions clés :

  • Elle scanne les classes annotées avec `@Entity` pour configurer le contexte de persistance.
  • Elle configure automatiquement une source de données (`DataSource`), généralement une base de données H2 en mémoire par défaut.
  • Elle configure un `EntityManagerFactory` et un `PlatformTransactionManager`.
  • Elle rend disponibles les beans `JpaRepository` (ou d'autres interfaces héritant de `Repository`) présents dans votre projet.
  • Par défaut, chaque méthode de test s'exécute dans une transaction qui est annulée (rollback) à la fin du test, assurant ainsi l'isolation entre les tests.
  • Elle fournit un bean `TestEntityManager`, un outil très pratique pour interagir directement avec le contexte de persistance JPA sous-jacent dans vos tests (persister des entités, les vider dans la base, les retrouver).

Voici un exemple typique d'utilisation de `@DataJpaTest` :

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.data.jpa.repository.JpaRepository;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.util.Optional;

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

@DataJpaTest // Active la configuration de test JPA
class ProduitRepositoryTest {

    @Autowired
    private TestEntityManager entityManager; // Pour manipuler les entités directement

    @Autowired
    private ProduitRepository produitRepository; // Le repository à tester

    @Test
    void findById_devraitRetournerProduit_siExiste() {
        // Arrange: Persiste une entité de test via TestEntityManager
        Produit produit = new Produit("Laptop XYZ");
        Produit produitPersiste = entityManager.persistFlushFind(produit); // Persiste, flush et récupère

        // Act: Utilise le repository pour trouver l'entité
        Optional produitTrouveOpt = produitRepository.findById(produitPersiste.getId());

        // Assert: Vérifie que le produit a été trouvé et est correct
        assertThat(produitTrouveOpt).isPresent();
        assertThat(produitTrouveOpt.get().getNom()).isEqualTo("Laptop XYZ");
    }

    @Test
    void save_devraitPersisterProduit() {
        // Arrange
        Produit nouveauProduit = new Produit("Souris ABC");

        // Act: Sauvegarde via le repository
        Produit produitSauvegarde = produitRepository.save(nouveauProduit);

        // Assert: Vérifie que l'ID a été généré et que l'entité existe dans la DB (via EM)
        assertThat(produitSauvegarde.getId()).isNotNull();
        Produit produitRecupere = entityManager.find(Produit.class, produitSauvegarde.getId());
        assertThat(produitRecupere).isNotNull();
        assertThat(produitRecupere.getNom()).isEqualTo("Souris ABC");
    }
}

// --- Définitions (normalement dans src/main/java) ---
@Entity
class Produit {
    @Id @GeneratedValue
    private Long id;
    private String nom;
    protected Produit() {} // Requis par JPA
    public Produit(String nom) { this.nom = nom; }
    public Long getId() { return id; }
    public String getNom() { return nom; }
}

interface ProduitRepository extends JpaRepository {}

// Nécessite une classe @SpringBootConfiguration minimale ou une structure de projet standard
@org.springframework.boot.SpringBootConfiguration
class TestJpaApplication {}

Le `TestEntityManager` est particulièrement utile pour préparer l'état de la base de données avant l'exécution de la méthode du repository que vous souhaitez tester, et pour vérifier l'état final après l'exécution.

@DataJdbcTest : tester vos repositories Spring Data JDBC

Si vous utilisez Spring Data JDBC, l'alternative plus simple à JPA, l'annotation `@DataJdbcTest` est celle qu'il vous faut. Elle est similaire à `@DataJpaTest` mais adaptée aux spécificités de Spring Data JDBC :

  • Elle configure une `DataSource` (base H2 en mémoire par défaut).
  • Elle configure un `JdbcTemplate` et un `NamedParameterJdbcTemplate` qui peuvent être injectés dans vos tests.
  • Elle configure un `PlatformTransactionManager`.
  • Elle scanne et configure les beans `Repository` spécifiques à Spring Data JDBC.
  • Comme `@DataJpaTest`, elle exécute chaque test dans une transaction annulée par défaut.

L'utilisation ressemble beaucoup à celle de `@DataJpaTest`, mais sans `TestEntityManager`. Vous interagissez principalement avec le repository injecté et potentiellement avec `JdbcTemplate` pour des vérifications ou une préparation plus fines.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.data.annotation.Id;
import org.springframework.data.repository.CrudRepository;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Optional;

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

@DataJdbcTest // Active la configuration de test Spring Data JDBC
class ClientRepositoryTest {

    @Autowired
    private ClientRepository clientRepository; // Le repository à tester

    @Autowired
    private JdbcTemplate jdbcTemplate; // Pour des vérifications directes si besoin

    @Test
    void save_devraitInsererClient() {
        // Arrange
        Client nouveauClient = new Client(null, "Alice Martin"); // L'ID est null avant insertion

        // Act
        Client clientSauvegarde = clientRepository.save(nouveauClient);

        // Assert
        assertThat(clientSauvegarde.id()).isNotNull(); // Vérifie que l'ID a été généré
        assertThat(clientSauvegarde.nom()).isEqualTo("Alice Martin");

        // Vérification avec JdbcTemplate (optionnel)
        Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM client WHERE id = ?", Integer.class, clientSauvegarde.id());
        assertThat(count).isEqualTo(1);
    }

    @Test
    void findById_devraitRetournerClient_siExiste() {
        // Arrange: Insère des données de test via le repository
        Client clientInsere = clientRepository.save(new Client(null, "Bob Durand"));

        // Act
        Optional clientTrouveOpt = clientRepository.findById(clientInsere.id());

        // Assert
        assertThat(clientTrouveOpt).isPresent();
        assertThat(clientTrouveOpt.get().nom()).isEqualTo("Bob Durand");
    }
}

// --- Définitions (normalement dans src/main/java) ---
// Classe record immuable pour l'entité JDBC
record Client(@Id Long id, String nom) {}

// Repository Spring Data JDBC
interface ClientRepository extends CrudRepository {}

// Schéma SQL (requis pour H2 avec Data JDBC)
// Placez un fichier schema.sql dans src/test/resources
// CREATE TABLE client (id IDENTITY PRIMARY KEY, nom VARCHAR(255));

// Nécessite une classe @SpringBootConfiguration minimale ou une structure de projet standard
@org.springframework.boot.SpringBootConfiguration
class TestJdbcApplication {}

Notez que pour Spring Data JDBC avec H2, vous devez souvent fournir un fichier `schema.sql` dans `src/test/resources` pour créer la structure de table nécessaire, car contrairement à JPA/Hibernate, il n'y a pas de génération automatique de schéma basée sur les entités par défaut.

@DataMongoTest : tester vos repositories MongoDB

Pour les applications utilisant MongoDB, `@DataMongoTest` fournit l'environnement de test adéquat :

  • Elle scanne les classes annotées avec `@Document`.
  • Elle configure un bean `MongoTemplate` prêt à l'emploi.
  • Elle configure les beans `MongoRepository`.
  • Par défaut, elle essaie de se connecter à une base de données MongoDB embarquée (si la dépendance `de.flapdoodle.embed.mongo` est présente) ou à une instance MongoDB locale sur le port standard (27017).
  • Contrairement à `@DataJpaTest` et `@DataJdbcTest`, les tests ne sont pas transactionnels par défaut car MongoDB ne supporte pas les transactions globales de la même manière que les bases relationnelles. Vous devrez gérer le nettoyage des données manuellement si nécessaire (par exemple, en supprimant les documents créés dans une méthode `@AfterEach`).

Exemple de test avec `@DataMongoTest` :

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;
import java.util.Optional;

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

@DataMongoTest // Active la configuration de test MongoDB
class CommandeRepositoryTest {

    @Autowired
    private MongoTemplate mongoTemplate; // Pour interactions directes ou nettoyage

    @Autowired
    private CommandeRepository commandeRepository; // Le repository à tester

    @AfterEach
    void tearDown() {
        // Nettoyage manuel car pas de rollback transactionnel
        mongoTemplate.dropCollection(Commande.class);
    }

    @Test
    void save_devraitEnregistrerCommande() {
        // Arrange
        Commande nouvelleCommande = new Commande("CMD001", 150.75);

        // Act
        Commande commandeSauvegardee = commandeRepository.save(nouvelleCommande);

        // Assert
        assertThat(commandeSauvegardee.getId()).isNotNull();
        Commande commandeTrouvee = mongoTemplate.findById(commandeSauvegardee.getId(), Commande.class);
        assertThat(commandeTrouvee).isNotNull();
        assertThat(commandeTrouvee.getNumero()).isEqualTo("CMD001");
    }

    @Test
    void findByNumero_devraitRetournerCommande() {
        // Arrange: Enregistre une commande de test
        mongoTemplate.save(new Commande("CMD002", 99.99));

        // Act
        Optional commandeTrouveeOpt = commandeRepository.findByNumero("CMD002");

        // Assert
        assertThat(commandeTrouveeOpt).isPresent();
        assertThat(commandeTrouveeOpt.get().getMontant()).isEqualTo(99.99);
    }
}

// --- Définitions (normalement dans src/main/java) ---
@Document(collection = "commandes")
class Commande {
    @Id
    private String id;
    private String numero;
    private double montant;
    public Commande(String numero, double montant) { this.numero = numero; this.montant = montant; }
    // Getters...
    public String getId() { return id; }
    public String getNumero() { return numero; }
    public double getMontant() { return montant; }
}

interface CommandeRepository extends MongoRepository {
    Optional findByNumero(String numero);
}

// Nécessite une classe @SpringBootConfiguration minimale ou une structure de projet standard
@org.springframework.boot.SpringBootConfiguration
class TestMongoApplication {}

Assurez-vous d'inclure la dépendance `de.flapdoodle.embed.mongo` dans votre `pom.xml` ou `build.gradle` (avec la scope `test`) si vous souhaitez utiliser une base MongoDB embarquée automatiquement.

Personnalisation et considérations communes

Bien que les configurations par défaut soient pratiques, vous pouvez les personnaliser :

  • Base de données : Pour utiliser une base de données différente de H2 (pour JPA/JDBC) ou configurer la connexion MongoDB, vous pouvez utiliser l'annotation `@AutoConfigureTestDatabase`. Par exemple, `@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)` indique à Spring Boot de ne pas remplacer la `DataSource` configurée dans vos `application.properties` (souvent utilisé avec Testcontainers pour lancer une vraie base de données dans un conteneur Docker).
  • Transactions (JPA/JDBC) : Si vous ne voulez pas le comportement de rollback automatique (par exemple, pour tester le fonctionnement du commit), vous pouvez annoter votre méthode de test ou la classe avec `@Transactional(propagation = Propagation.NOT_SUPPORTED)` ou désactiver la gestion transactionnelle via les propriétés de test.
  • Beans supplémentaires : Si votre couche de persistance dépend d'autres beans (par exemple, des convertisseurs, des listeners) qui ne sont pas automatiquement chargés par ces annotations, vous pouvez les inclure explicitement en utilisant `@Import(MonBeanSupplementaire.class)` sur votre classe de test.
  • Initialisation des données : Vous pouvez utiliser des fichiers `data.sql` (pour JPA/JDBC) ou des `ApplicationRunner`/`CommandLineRunner` spécifiques au profil de test pour peupler la base de données avant les tests, en complément ou en alternative à la préparation dans les méthodes `@BeforeEach` ou directement dans les tests via `TestEntityManager` ou `MongoTemplate`.

En conclusion, `@DataJpaTest`, `@DataJdbcTest` et `@DataMongoTest` sont des outils puissants et spécialisés pour tester efficacement votre couche de persistance dans Spring Boot. Ils offrent un bon équilibre entre réalisme (en utilisant les composants Spring Data) et performance (en chargeant uniquement le contexte nécessaire), tout en fournissant des utilitaires et des configurations par défaut qui simplifient grandement l'écriture de ces tests essentiels.