web-dev-qa-db-fra.com

Comment tester un composant / bean dans Spring Boot

Pour tester un composant/bean dans une application Spring Boot, la partie test de la documentation Spring Boot fournit de nombreuses informations et de plusieurs façons: @Test, @SpringBootTest, @WebMvcTest, @DataJpaTest et encore de nombreuses autres façons.
Pourquoi fournir autant de moyens? Comment décider de la façon de favoriser?
Devrais-je considérer comme des tests d'intégration mes classes de test annotées avec des annotations de test Spring Boot telles que @SpringBootTest, @WebMvcTest, @DataJpaTest?

PS: J'ai créé cette question parce que j'ai remarqué que beaucoup de développeurs (même expérimentés) ne subissent pas les conséquences d'utiliser une annotation plutôt qu'une autre.

15
davidxxx

TL-DR

  • rédigez des tests unitaires simples pour les composants que vous pouvez tester directement sans charger de conteneur Spring (exécutez-les en local et dans la génération de CI).
  • écrire des tests d'intégration partiels/ test unitaire de tranchage pour les composants que vous ne pouvez pas tester directement sans charger un conteneur Spring , tels que les composants liés à JPA, contrôleurs, REST), JDBC ... (exécutez-les en version locale et dans la génération de CI)

  • écrire des tests d'intégration complets (tests de bout en bout) pour certains composants de haut niveau où il apporte des valeurs (les exécuter dans la construction de CI).


3 façons principales de tester un composant

  • test unitaire simple (ne charge pas un conteneur Spring)
  • test d'intégration complète (chargez un conteneur Spring avec toute la configuration et les beans)
  • test d'intégration partielle/test de tranchage (chargez un conteneur Spring avec des configurations et des beans très restreints)

Tous les composants peuvent-ils être testés de ces 3 manières?

D'une manière générale avec Spring, tout composant peut être testé lors de tests d'intégration et seuls certains types de composants peuvent être testés de manière unitaire (sans conteneur).
Mais notons que avec ou sans ressort, les tests unitaires et d'intégration ne sont pas opposés mais complémentaires.

Écrire un test unitaire simple

L'utilisation de Spring Boot dans votre application ne signifie pas que vous devez charger le conteneur Spring pour toute classe de test que vous exécutez.
Lorsque vous écrivez un test qui ne nécessite aucune dépendance du conteneur Spring, , vous n'avez pas pour utiliser/charger Spring. dans la classe de test.
Au lieu d'utiliser Spring, vous allez instancier vous-même la classe à tester et, si nécessaire, utilisez une bibliothèque fictive pour isoler l'instance testée de ses dépendances.
C’est la voie à suivre car elle est rapide et favorise l’isolement du composant testé.
Par exemple, un FooService annoté en tant que service Spring qui effectue certains calculs et qui repose sur FooRepository pour récupérer des données peut être testé sans Spring:

@Service
public class FooService{
   private FooRepository fooRepository;

   public FooService(FooRepository fooRepository){
       this.fooRepository = fooRepository;
   }

   public long compute(...){
      List<Foo> foos = fooRepository.findAll(...);
       // core logic
      long result = 
           foos.stream()
               .map(Foo::getValue)
               .filter(v->...)
               .count();
       return result;
   }
}

Vous pouvez simuler FooRepository et tester un peu la logique de FooService.
Avec JUnit 5 et Mockito, la classe de test pourrait ressembler à:

import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;


@ExtendWith(MockitoExtension.class)
class FooServiceTest{

    FooService fooService;  

    @Mock
    FooRepository fooRepository;

    @BeforeEach 
    void init{
        fooService = new FooService(fooRepository);
    }

    @Test
    void compute(){
        List<Foo> fooData = ...;
        Mockito.when(fooRepository.findAll(...))
               .thenReturn(fooData);
        long actualResult = fooService.compute(...);
        long expectedResult = ...;
        Assertions.assertEquals(expectedResult, actualResult);
    }

}

Comment déterminer si un composant peut être testé dans son ensemble (sans ressort) ou uniquement avec Spring?

Vous reconnaissez un code à tester ne dépendant d'aucun conteneur Spring, car le composant/la méthode n'utilise pas la fonctionnalité Spring pour effectuer sa logique.
Dans l'exemple précédent, FooService effectue des calculs et une logique qui n'exigent pas que Spring soit exécuté.
En effet, avec ou sans conteneur, la méthode compute() contient la logique de base que nous voulons affirmer.
Inversement, vous aurez des difficultés à tester FooRepository sans Spring as Spring Boot configure pour vous la source de données, le contexte JPA et instrumentez votre interface FooRepository pour lui fournir une implémentation par défaut. et plusieurs autres choses.
Même chose pour tester un contrôleur (repos ou MVC).
Comment un contrôleur peut-il être lié à un noeud final sans Spring? Comment le contrôleur pourrait-il analyser la requête HTTP et générer une réponse HTTP sans Spring? Ça ne peut tout simplement pas être fait.

Rédaction d'un test d'intégration complet

L'écriture d'un test de bout en bout nécessite de charger un conteneur avec la configuration complète et les beans de l'application.
Pour y parvenir @SpringBootTest , voici le chemin:

L'annotation fonctionne en créant le contexte d'application utilisé dans vos tests via SpringApplication.

Vous pouvez l'utiliser de cette manière pour le tester sans aucune maquette:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class FooTest {

   @Autowired
   Foo foo;

   @Test
   public void doThat(){
      FooBar fooBar = foo.doThat(...);
      // assertion...
   }    

}

Mais vous pouvez également vous moquer des haricots du conteneur si cela vous semble logique:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

@SpringBootTest
public class FooTest {

   @Autowired
   Foo foo;

   @MockBean
   private Bar barDep;

   @Test
   public void doThat(){
      Mockito.when(barDep.doThis()).thenReturn(...);
      FooBar fooBar = foo.doThat(...);
      // assertion...
   }    

}

Notez la différence lorsque vous vous moquez lorsque vous voulez simuler une instance simple d'une classe Bar (annotation org.mockito.Mock) Et que vous souhaitez vous moquer d'un bean Bar du contexte Spring ( org.springframework.boot.test.mock.mockito.MockBean Annotation).

Les tests d'intégration complets doivent être exécutés par les versions de CI.

Charger un contexte de printemps complet prend du temps. Vous devez donc être prudent avec @SpringBootTest Car cela peut rendre l'exécution de tests unitaires très longue et en général vous ne voulez pas ralentir fortement la construction locale sur la machine du développeur et le retour de test qui compte pour rendre le test d'écriture agréable et efficace pour les développeurs.
C'est pourquoi les tests "lents" ne sont généralement pas exécutés sur les machines du développeur.
Vous devez donc leur faire des tests d'intégration (suffixe IT à la place de suffixe Test dans la dénomination de la classe de test) et assurez-vous qu'ils ne sont exécutés que dans les versions à intégration continue. .
Mais comme Spring Boot agit sur de nombreux éléments de votre application (contrôleurs rest, contrôleurs MVC, sérialisation/désérialisation JSON, persistance, etc.), vous pouvez écrire de nombreux tests unitaires exécutés uniquement sur le CI. construit et ce n’est pas bien non plus.
Le fait d'exécuter des tests de bout en bout uniquement sur les versions de CI est correct, mais il est également incorrect d'avoir des tests de persistance, de contrôleurs ou JSON exécutés uniquement sur les versions de CI.
En effet, la construction du développeur sera rapide mais l’inconvénient est que l’exécution des tests en local ne détectera qu’une petite partie des régressions possibles ...
Pour éviter cette mise en garde, Spring Boot fournit une méthode intermédiaire: test d'intégration partiel ou test de tranche (comme ils l'appellent): le point suivant.

Rédaction d'un test d'intégration partiel axé sur une couche ou un problème spécifique (test de tranche)

Comme expliqué au point "Reconnaître un test qui peut être testé simplement (sans ressort))", certains composants ne peuvent être testés qu'avec un conteneur en fonctionnement.
Mais pourquoi utiliser @SpringBootTest Pour charger tous les beans et toutes les configurations de votre application alors que vous n’auriez besoin que de charger quelques classes de configuration et beans spécifiques pour tester ces composants?
Par exemple, pourquoi charger un contexte Spring JPA complet (beans, configurations, dans une base de données en mémoire, etc.) pour tester un contrôleur unitaire?
Et inversement, pourquoi charger toutes les configurations et tous les beans associés aux contrôleurs Spring afin de tester de manière unitaire un référentiel JPA?
Spring Boot résout ce problème avec le fonctionnalité de test de tranche .
Ce ne sont pas aussi rapides que des tests unitaires simples (sans conteneur), mais ils sont vraiment beaucoup plus rapides que de charger tout le contexte. Donc, les exécuter sur la machine locale est généralement très acceptable .
Chaque type d’essai de test charge un ensemble très restreint de classes de configuration automatique que vous pouvez modifier si nécessaire en fonction de vos besoins.

Quelques fonctionnalités de test de tranche courantes:

Pour tester cet objet, la sérialisation et la désérialisation JSON fonctionne comme prévu, vous pouvez utiliser l'annotation @JsonTest.

Pour vérifier si les contrôleurs Spring MVC fonctionnent comme prévu, utilisez l'annotation @WebMvcTest.

Pour vérifier que les contrôleurs Spring WebFlux fonctionnent comme prévu, vous pouvez utiliser l'annotation @WebFluxTest.

Vous pouvez utiliser l'annotation @DataJpaTest Pour tester les applications JPA.

Et vous avez encore de nombreux autres types de tranches que Spring Boot vous fournit.
Voir la partie "test" de la documentation pour obtenir plus de détails.

27
davidxxx