JUnit 5-Tutorial, Teil 2: Unit-Test Spring MVC mit JUnit 5

Spring MVC ist eines der beliebtesten Java-Frameworks zum Erstellen von Java-Unternehmensanwendungen und eignet sich sehr gut zum Testen. Spring MVC fördert von Natur aus die Trennung von Bedenken und die Codierung gegen Schnittstellen. Diese Eigenschaften und die Implementierung der Abhängigkeitsinjektion durch Spring machen Spring-Anwendungen sehr testbar.

Dieses Tutorial ist die zweite Hälfte meiner Einführung in Unit-Tests mit JUnit 5. Ich zeige Ihnen, wie Sie JUnit 5 in Spring integrieren, und stelle Ihnen dann drei Tools vor, mit denen Sie Spring MVC-Controller, -Dienste und -Repositorys testen können.

download Code herunterladen Laden Sie den Quellcode herunter, z. B. die in diesem Lernprogramm verwendeten Anwendungen. Erstellt von Steven Haines für JavaWorld.

Integration von JUnit 5 in Spring 5

In diesem Tutorial verwenden wir Maven und Spring Boot. Als Erstes müssen wir die JUnit 5-Abhängigkeit zu unserer Maven POM-Datei hinzufügen:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Genau wie in Teil 1 verwenden wir für dieses Beispiel Mockito. Wir müssen also die JUnit 5 Mockito-Bibliothek hinzufügen:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith und die SpringExtension-Klasse

JUnit 5 definiert eine Erweiterungsschnittstelle , über die Klassen in verschiedenen Phasen des Ausführungslebenszyklus in JUnit-Tests integriert werden können. Wir können Erweiterungen aktivieren, indem wir die @ExtendWithAnnotation zu unseren Testklassen hinzufügen und die zu ladende Erweiterungsklasse angeben. Die Erweiterung kann dann verschiedene Rückrufschnittstellen implementieren, die während des gesamten Testlebenszyklus aufgerufen werden: vor allen Testläufen, vor jedem Testlauf, nach jedem Testlauf und nachdem alle Tests ausgeführt wurden.

Spring definiert eine SpringExtensionKlasse, die JUnit 5-Lebenszyklusbenachrichtigungen abonniert, um einen "Testkontext" zu erstellen und zu verwalten. Denken Sie daran, dass der Anwendungskontext von Spring alle Spring Beans in einer Anwendung enthält und eine Abhängigkeitsinjektion durchführt, um eine Anwendung und ihre Abhängigkeiten miteinander zu verbinden. Spring verwendet das JUnit 5-Erweiterungsmodell, um den Anwendungskontext des Tests beizubehalten, wodurch das Schreiben von Komponententests mit Spring unkompliziert wird.

Nachdem wir die JUnit 5-Bibliothek zu unserer Maven POM-Datei hinzugefügt haben, können wir die verwenden SpringExtension.class, um unsere JUnit 5-Testklassen zu erweitern:

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

Das Beispiel ist in diesem Fall eine Spring Boot-Anwendung. Glücklicherweise enthält die @SpringBootTestAnnotation bereits die @ExtendWith(SpringExtension.class)Annotation, sodass wir sie nur einschließen müssen @SpringBootTest.

Hinzufügen der Mockito-Abhängigkeit

Um jede Komponente isoliert zu testen und verschiedene Szenarien zu simulieren, möchten wir Scheinimplementierungen der Abhängigkeiten jeder Klasse erstellen. Hier kommt Mockito ins Spiel. Fügen Sie die folgende Abhängigkeit in Ihre POM-Datei ein, um Unterstützung für Mockito hinzuzufügen:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Nachdem Sie JUnit 5 und Mockito in Ihre Spring-Anwendung integriert haben, können Sie Mockito nutzen, indem Sie einfach eine Spring-Bean (z. B. einen Service oder ein Repository) in Ihrer Testklasse mithilfe der @MockBeanAnmerkung definieren. Hier ist unser Beispiel:

 @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; ... } 

In diesem Beispiel erstellen wir ein Modell WidgetRepositoryinnerhalb unserer WidgetServiceTestKlasse. Wenn Spring dies sieht, wird es automatisch mit unserem verbunden, WidgetServicesodass wir in unseren Testmethoden verschiedene Szenarien erstellen können. Jede Testmethode konfiguriert das Verhalten von WidgetRepository, z. B. indem die angeforderte Widgetoder eine Optional.empty()für eine Abfrage zurückgegeben wird, für die die Daten nicht gefunden werden. Wir werden den Rest dieses Tutorials damit verbringen, Beispiele für verschiedene Möglichkeiten zum Konfigurieren dieser Mock Beans zu betrachten.

Die Spring MVC-Beispielanwendung

Um Spring-basierte Unit-Tests zu schreiben, benötigen wir eine Anwendung, gegen die sie geschrieben werden können. Glücklicherweise können wir die Beispielanwendung aus meinem Spring Series- Tutorial "Mastering Spring Framework 5, Teil 1: Spring MVC" verwenden. Ich habe die Beispielanwendung aus diesem Tutorial als Basisanwendung verwendet. Ich habe es mit einer stärkeren REST-API modifiziert, damit wir noch ein paar Dinge testen müssen.

Die Beispielanwendung ist eine Spring MVC-Webanwendung mit einem REST-Controller, einer Serviceschicht und einem Repository, das Spring Data JPA verwendet, um "Widgets" zu und von einer H2-In-Memory-Datenbank beizubehalten. Abbildung 1 ist eine Übersicht.

Steven Haines

Was ist ein Widget?

A Widgetist nur ein "Ding" mit einer ID, einem Namen, einer Beschreibung und einer Versionsnummer. In diesem Fall wird unser Widget mit JPA-Anmerkungen versehen, um es als Entität zu definieren. Das WidgetRestControllerist ein Spring MVC - Controller, die RESTful - API - Aufrufe übersetzen in Aktionen auf ausführen Widgets. Dies WidgetServiceist ein Standard-Spring-Service, für den Geschäftsfunktionen definiert werden Widgets. Schließlich WidgetRepositoryhandelt es sich um eine Spring Data JPA-Schnittstelle, für die Spring zur Laufzeit eine Implementierung erstellt. Wir werden den Code für jede Klasse überprüfen, während wir in den nächsten Abschnitten Tests schreiben.

Unit-Test eines Spring-Service

Beginnen wir mit der Überprüfung des Testens eines Spring-  Dienstes , da dies die am einfachsten zu testende Komponente in unserer MVC-Anwendung ist. Beispiele in diesem Abschnitt ermöglichen es uns, die Integration von JUnit 5 in Spring zu untersuchen, ohne neue Testkomponenten oder Bibliotheken einzuführen. Dies werden wir jedoch später im Lernprogramm tun.

Wir beginnen mit der Überprüfung der WidgetServiceSchnittstelle und der WidgetServiceImplKlasse, die in Listing 1 bzw. Listing 2 gezeigt werden.

Listing 1. Die Spring-Service-Oberfläche (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. Die Spring Service-Implementierungsklasse (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); } }

WidgetServiceImplist ein Spring-Service, der mit der @ServiceAnnotation versehen ist WidgetRepositoryund über dessen Konstruktor ein Kabel angeschlossen ist. Die findById(), findAll()und deleteById()Methoden sind alle Pass - Through - Verfahren auf die darunter liegende WidgetRepository. Die einzige Geschäftslogik, die Sie finden, befindet sich in der save()Methode, die die Versionsnummer Widgetbeim Speichern erhöht .

Die Testklasse

Um diese Klasse zu testen, müssen wir ein Modell erstellen und konfigurieren WidgetRepository, es mit der WidgetServiceImplInstanz verbinden und dann mit WidgetServiceImplunserer Testklasse verbinden. Zum Glück ist das viel einfacher als es sich anhört. Listing 3 zeigt den Quellcode für die WidgetServiceTestKlasse.

Listing 3. Die Spring-Service-Testklasse (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"); } } 

The WidgetServiceTest class is annotated with the @SpringBootTest annotation, which scans the CLASSPATH for all Spring configuration classes and beans and sets up the Spring application context for the test class. Note that WidgetServiceTest also implicitly includes the @ExtendWith(SpringExtension.class) annotation, through the @SpringBootTest annotation, which integrates the test class with JUnit 5.

The test class also uses Spring's @Autowired annotation to autowire a WidgetService to test against, and it uses Mockito's @MockBean annotation to create a mock WidgetRepository. At this point, we have a mock WidgetRepository that we can configure, and a real WidgetService with the mock WidgetRepository wired into it.

Testing the Spring service

Die erste Testmethode testFindById()führt WidgetServicedie findById()Methode aus, die eine zurückgeben soll Optional, die a enthält Widget. Wir beginnen mit der Erstellung eines Widget, das wir zurückgeben möchten WidgetRepository. Anschließend nutzen wir die Mockito-API, um die WidgetRepository::findByIdMethode zu konfigurieren . Die Struktur unserer Scheinlogik ist wie folgt:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

In diesem Fall sagen wir: Geben Sie eine Optionalunserer zurück, Widgetwenn die findById()Methode des Repositorys mit dem Argument 1 (als a long) aufgerufen wird .

Als nächstes wir die invoke WidgetServices‘ findByIdMethode mit einem Argument von 1. Wir dann bestätigen , dass es vorhanden ist , und dass der zurück Widgetist der, daß wir die mock konfiguriert WidgetRepositoryzurückzukehren.