Tutoriel JUnit 5, partie 2: Test unitaire de Spring MVC avec JUnit 5

Spring MVC est l'un des frameworks Java les plus populaires pour la création d'applications Java d'entreprise et se prête très bien aux tests. De par sa conception, Spring MVC favorise la séparation des préoccupations et encourage le codage par rapport aux interfaces. Ces qualités, ainsi que l'implémentation par Spring de l'injection de dépendances, rendent les applications Spring très testables.

Ce didacticiel est la seconde moitié de mon introduction aux tests unitaires avec JUnit 5. Je vais vous montrer comment intégrer JUnit 5 avec Spring, puis vous présenter trois outils que vous pouvez utiliser pour tester les contrôleurs, services et référentiels Spring MVC.

télécharger Obtenir le code Téléchargez le code source des exemples d'applications utilisées dans ce didacticiel. Créé par Steven Haines pour JavaWorld.

Intégration de JUnit 5 avec Spring 5

Pour ce tutoriel, nous utilisons Maven et Spring Boot, donc la première chose que nous devons faire est d'ajouter la dépendance JUnit 5 à notre fichier Maven POM:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Tout comme nous l'avons fait dans la partie 1, nous utiliserons Mockito pour cet exemple. Nous allons donc devoir ajouter la bibliothèque JUnit 5 Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith et la classe SpringExtension

JUnit 5 définit une interface d'extension , à travers laquelle les classes peuvent s'intégrer aux tests JUnit à différentes étapes du cycle de vie de l'exécution. Nous pouvons activer les extensions en ajoutant l' @ExtendWithannotation à nos classes de test et en spécifiant la classe d'extension à charger. L'extension peut ensuite implémenter diverses interfaces de rappel, qui seront appelées tout au long du cycle de vie du test: avant l'exécution de tous les tests, avant l'exécution de chaque test, après l'exécution de chaque test et après l'exécution de tous les tests.

Spring définit une SpringExtensionclasse qui s'abonne aux notifications de cycle de vie JUnit 5 pour créer et maintenir un «contexte de test». Rappelez-vous que le contexte d'application de Spring contient tous les beans Spring d'une application et qu'il effectue une injection de dépendances pour relier une application et ses dépendances. Spring utilise le modèle d'extension JUnit 5 pour maintenir le contexte d'application du test, ce qui facilite l'écriture de tests unitaires avec Spring.

Après avoir ajouté la bibliothèque JUnit 5 à notre fichier Maven POM, nous pouvons utiliser le SpringExtension.classpour étendre nos classes de test JUnit 5:

 @ExtendWith(SpringExtension.class) class MyTests { // ... }

L'exemple, dans ce cas, est une application Spring Boot. Heureusement, l' @SpringBootTestannotation inclut déjà l' @ExtendWith(SpringExtension.class)annotation, il suffit donc de l'inclure @SpringBootTest.

Ajout de la dépendance Mockito

Pour tester correctement chaque composant de manière isolée et simuler différents scénarios, nous allons vouloir créer des implémentations simulées des dépendances de chaque classe. Voici où Mockito entre en jeu. Incluez la dépendance suivante dans votre fichier POM pour ajouter la prise en charge de Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Après avoir intégré JUnit 5 et Mockito dans votre application Spring, vous pouvez tirer parti de Mockito en définissant simplement un bean Spring (tel qu'un service ou un référentiel) dans votre classe de test à l'aide de l' @MockBeanannotation. Voici notre exemple:

 @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; ... } 

Dans cet exemple, nous créons une maquette WidgetRepositorydans notre WidgetServiceTestclasse. Lorsque Spring voit cela, il le connecte automatiquement à notre WidgetServiceafin que nous puissions créer différents scénarios dans nos méthodes de test. Chaque méthode de test configurera le comportement du WidgetRepository, par exemple en renvoyant le demandé Widgetou en renvoyant un Optional.empty()pour une requête pour laquelle les données ne sont pas trouvées. Nous passerons le reste de ce didacticiel à examiner des exemples de différentes façons de configurer ces haricots fictifs.

L'exemple d'application Spring MVC

Pour écrire des tests unitaires basés sur Spring, nous avons besoin d'une application sur laquelle les écrire. Heureusement, nous pouvons utiliser l'exemple d'application de mon tutoriel Spring Series «Mastering Spring framework 5, Part 1: Spring MVC». J'ai utilisé l'exemple d'application de ce tutoriel comme application de base. Je l'ai modifié avec une API REST plus puissante pour que nous ayons quelques autres choses à tester.

L'exemple d'application est une application Web Spring MVC avec un contrôleur REST, une couche de service et un référentiel qui utilise Spring Data JPA pour conserver les «widgets» vers et depuis une base de données en mémoire H2. La figure 1 est un aperçu.

Steven Haines

Qu'est-ce qu'un widget?

A Widgetest juste une «chose» avec un ID, un nom, une description et un numéro de version. Dans ce cas, notre widget est annoté avec des annotations JPA pour le définir comme une entité. Il WidgetRestControllers'agit d'un contrôleur Spring MVC qui traduit les appels d'API RESTful en actions à exécuter Widgets. Le WidgetServiceest un service Spring standard qui définit les fonctionnalités métier pour Widgets. Enfin, il WidgetRepositorys'agit d'une interface Spring Data JPA, pour laquelle Spring créera une implémentation au moment de l'exécution. Nous examinerons le code de chaque classe au fur et à mesure que nous écrivons des tests dans les sections suivantes.

Test unitaire d'un service Spring

Commençons par examiner comment tester un service Spring  , car c'est le composant le plus simple à tester de notre application MVC. Les exemples de cette section nous permettront d'explorer l'intégration de JUnit 5 avec Spring sans introduire de nouveaux composants ou bibliothèques de test, bien que nous le ferons plus tard dans le didacticiel.

Nous commencerons par passer en revue l' WidgetServiceinterface et la WidgetServiceImplclasse, qui sont présentées respectivement dans le Listing 1 et le Listing 2.

Listing 1. L'interface du service Spring (WidgetService.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import java.util.List; import java.util.Optional; public interface WidgetService { Optional findById(Long id); List findAll(); Widget save(Widget widget); void deleteById(Long id); }

Listing 2. La classe d'implémentation du service Spring (WidgetServiceImpl.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import com.google.common.collect.Lists; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Service public class WidgetServiceImpl implements WidgetService { private WidgetRepository repository; public WidgetServiceImpl(WidgetRepository repository) { this.repository = repository; } @Override public Optional findById(Long id) { return repository.findById(id); } @Override public List findAll() { return Lists.newArrayList(repository.findAll()); } @Override public Widget save(Widget widget) { // Increment the version number widget.setVersion(widget.getVersion()+1); // Save the widget to the repository return repository.save(widget); } @Override public void deleteById(Long id) { repository.deleteById(id); } }

WidgetServiceImplest un service Spring, annoté avec l' @Serviceannotation, qui est WidgetRepositorycâblé via son constructeur. Les findById(), findAll()et les deleteById()méthodes sont toutes les méthodes passthrough au sous - jacent WidgetRepository. La seule logique métier que vous trouverez se trouve dans la save()méthode, qui incrémente le numéro de version du Widgetlors de son enregistrement.

La classe de test

Afin de tester cette classe, nous devons créer et configurer un simulacre WidgetRepository, le câbler dans l' WidgetServiceImplinstance, puis câbler le WidgetServiceImpldans notre classe de test. Heureusement, c'est beaucoup plus facile qu'il n'y paraît. Le listing 3 montre le code source de la WidgetServiceTestclasse.

Listing 3. La classe de test de service Spring (WidgetServiceTest.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Arrays; import java.util.List; import java.util.Optional; import static org.mockito.Mockito.doReturn; import static org.mockito.ArgumentMatchers.any; @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; @Test @DisplayName("Test findById Success") void testFindById() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(Optional.of(widget)).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found"); Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock"); } @Test @DisplayName("Test findById Not Found") void testFindByIdNotFound() { // Setup our mock repository doReturn(Optional.empty()).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found"); } @Test @DisplayName("Test findAll") void testFindAll() { // Setup our mock repository Widget widget1 = new Widget(1l, "Widget Name", "Description", 1); Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4); doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll(); // Execute the service call List widgets = service.findAll(); // Assert the response Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets"); } @Test @DisplayName("Test save widget") void testSave() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(widget).when(repository).save(any()); // Execute the service call Widget returnedWidget = service.save(widget); // Assert the response Assertions.assertNotNull(returnedWidget, "The saved widget should not be null"); Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented"); } } 

La WidgetServiceTestclasse est annotée avec l' @SpringBootTestannotation, qui recherche CLASSPATHtoutes les classes de configuration Spring et tous les beans et configure le contexte d'application Spring pour la classe de test. Notez que WidgetServiceTestcomprend également implicitement l' @ExtendWith(SpringExtension.class)annotation, via l' @SpringBootTestannotation, qui intègre la classe de test avec JUnit 5.

La classe de test utilise également l' @Autowiredannotation de Spring pour lancer automatiquement un WidgetServicetest, et elle utilise l' @MockBeanannotation de Mockito pour créer une maquette WidgetRepository. À ce stade, nous avons une maquette WidgetRepositoryque nous pouvons configurer, et une vraie WidgetServiceavec la maquette WidgetRepositoryconnectée.

Tester le service Spring

La première méthode de test,, testFindById()exécute WidgetServicela findById()méthode de, qui doit renvoyer un Optionalqui contient un Widget. Nous commençons par créer un Widgetque nous voulons le WidgetRepositoryretourner. Nous utilisons ensuite l'API Mockito pour configurer la WidgetRepository::findByIdméthode. La structure de notre logique fictive est la suivante:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

Dans ce cas, nous disons: Renvoyez un Optionalde nos Widgetlorsque la findById()méthode du référentiel est appelée avec un argument de 1 (comme a long).

Ensuite, nous invoquons la méthode WidgetServices findByIdavec un argument de 1. Nous validons alors qu'elle est présente et que le retourné Widgetest celui que nous avons configuré le simulacre WidgetRepositoryde retour.