Samouczek JUnit 5, część 2: Testowanie jednostkowe Spring MVC z JUnit 5

Spring MVC jest jednym z najpopularniejszych frameworków Java do tworzenia aplikacji Java dla przedsiębiorstw i bardzo dobrze nadaje się do testowania. Z założenia Spring MVC promuje separację problemów i zachęca do kodowania w oparciu o interfejsy. Te cechy, wraz z implementacją wstrzykiwania zależności przez Spring, sprawiają, że aplikacje Spring są bardzo testowalne.

Ten samouczek to druga połowa mojego wprowadzenia do testów jednostkowych w JUnit 5. Pokażę ci, jak zintegrować JUnit 5 ze Spring, a następnie przedstawię trzy narzędzia, których możesz użyć do testowania kontrolerów, usług i repozytoriów Spring MVC.

pobierz Pobierz kod Pobierz kod źródłowy dla przykładowych aplikacji używanych w tym samouczku. Stworzone przez Stevena Hainesa dla JavaWorld.

Integracja JUnit 5 ze Spring 5

W tym samouczku używamy Maven i Spring Boot, więc pierwszą rzeczą, którą musimy zrobić, jest dodanie zależności JUnit 5 do naszego pliku POM Maven:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Podobnie jak w części 1, w tym przykładzie użyjemy Mockito. Będziemy więc musieli dodać bibliotekę JUnit 5 Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith i klasa SpringExtension

JUnit 5 definiuje interfejs rozszerzenia , za pomocą którego klasy mogą integrować się z testami JUnit na różnych etapach cyklu wykonania. Możemy włączyć rozszerzenia, dodając @ExtendWithadnotację do naszych klas testowych i określając klasę rozszerzenia do załadowania. Rozszerzenie może następnie implementować różne interfejsy wywołań zwrotnych, które będą wywoływane przez cały cykl życia testu: przed uruchomieniem wszystkich testów, przed każdym uruchomieniem testu, po każdym uruchomieniu testu i po zakończeniu wszystkich testów.

Spring definiuje SpringExtensionklasę, która subskrybuje powiadomienia cyklu życia JUnit 5 w celu utworzenia i utrzymania „kontekstu testowego”. Przypomnij sobie, że kontekst aplikacji Springa zawiera wszystkie ziarna Spring w aplikacji i wykonuje iniekcję zależności, aby połączyć aplikację i jej zależności. Spring używa modelu rozszerzenia JUnit 5, aby zachować kontekst aplikacji testu, co sprawia, że ​​pisanie testów jednostkowych w Spring jest proste.

Po dodaniu biblioteki JUnit 5 do naszego pliku POM Maven, możemy użyć SpringExtension.classrozszerzenia do rozszerzenia naszych klas testowych JUnit 5:

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

Przykładem w tym przypadku jest aplikacja Spring Boot. Na szczęście @SpringBootTestadnotacja już zawiera @ExtendWith(SpringExtension.class)adnotację, więc musimy ją tylko dołączyć @SpringBootTest.

Dodawanie zależności Mockito

Aby poprawnie przetestować każdy komponent w izolacji i symulować różne scenariusze, będziemy chcieli stworzyć symulowane implementacje zależności każdej klasy. Tutaj pojawia się Mockito. Uwzględnij następującą zależność w pliku POM, aby dodać obsługę Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Po zintegrowaniu JUnit 5 i Mockito z aplikacją Spring możesz wykorzystać Mockito, po prostu definiując komponent bean Spring (na przykład usługę lub repozytorium) w klasie testowej za pomocą @MockBeanadnotacji. Oto nasz przykład:

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

W tym przykładzie tworzymy makietę WidgetRepositorywewnątrz naszej WidgetServiceTestklasy. Kiedy Spring to zobaczy, automatycznie połączy go z naszym WidgetService, abyśmy mogli stworzyć różne scenariusze w naszych metodach testowych. Każda metoda testowa skonfiguruje zachowanie WidgetRepository, na przykład poprzez zwrócenie żądanego Widgetlub zwrócenie Optional.empty()zapytania, dla którego nie znaleziono danych. Pozostałą część tego samouczka spędzimy, przyglądając się przykładom różnych sposobów konfigurowania tych pozorowanych beanów.

Przykładowa aplikacja Spring MVC

Aby napisać testy jednostkowe oparte na Spring, potrzebujemy aplikacji, w odniesieniu do której będziemy je pisać. Na szczęście możemy skorzystać z przykładowej aplikacji z mojego samouczka Spring Series „Opanowanie struktury Spring 5, część 1: Spring MVC”. Użyłem przykładowej aplikacji z tego samouczka jako aplikacji podstawowej. Zmodyfikowałem go silniejszym API REST, abyśmy mieli kilka rzeczy do przetestowania.

Przykładową aplikacją jest aplikacja internetowa Spring MVC z kontrolerem REST, warstwą usług i repozytorium, które wykorzystuje Spring Data JPA do utrwalania "widżetów" do i z bazy danych H2 w pamięci. Rysunek 1 przedstawia przegląd.

Steven Haines

Co to jest widżet?

A Widgetto po prostu „rzecz” z identyfikatorem, nazwą, opisem i numerem wersji. W tym przypadku nasz widżet jest opatrzony adnotacjami JPA, aby zdefiniować go jako jednostkę. Jest WidgetRestControllerto kontroler Spring MVC, który tłumaczy wywołania RESTful API na akcje do wykonania Widgets. WidgetServiceJest standardową usługą Wiosna, która definiuje funkcjonalność biznesową Widgets. Wreszcie WidgetRepositoryjest to interfejs Spring Data JPA, dla którego Spring utworzy implementację w czasie wykonywania. W trakcie pisania testów w następnych sekcjach przejrzymy kod każdej klasy.

Testowanie jednostkowe usługi Spring

Zacznijmy od sprawdzenia, jak przetestować usługę Spring  , ponieważ jest to najłatwiejszy do przetestowania składnik w naszej aplikacji MVC. Przykłady w tej sekcji pozwolą nam zbadać integrację JUnit 5 ze Spring bez wprowadzania jakichkolwiek nowych komponentów testowych lub bibliotek, chociaż zrobimy to w dalszej części samouczka.

Zaczniemy od przeglądu WidgetServiceinterfejsu i WidgetServiceImplklasy, które są pokazane odpowiednio na Listingu 1 i Listingu 2.

Listing 1. Interfejs usługi 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. Klasa implementacji usługi 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); } }

WidgetServiceImpljest usługą Spring, opatrzoną @Serviceadnotacją, która ma połączenie WidgetRepositoryprzewodowe za pośrednictwem swojego konstruktora. Te findById(), findAll()oraz deleteById()metody są wszystkie metody passthrough dla zasadniczej WidgetRepository. Jedyna logika biznesowa, którą znajdziesz, znajduje się w save()metodzie, która zwiększa numer wersji, Widgetkiedy jest zapisywana.

Klasa testowa

Aby przetestować tę klasę, musimy utworzyć i skonfigurować makietę WidgetRepository, połączyć ją z WidgetServiceImplinstancją, a następnie połączyć ją z WidgetServiceImplnaszą klasą testową. Na szczęście jest to o wiele łatwiejsze niż się wydaje. Listing 3 przedstawia kod źródłowy WidgetServiceTestklasy.

Listing 3. Klasa testowa usługi 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"); } } 

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

Pierwszy metoda badawcza testFindById(), wykonuje WidgetServicejest findById()metoda, która powinna zwracać Optionalktóre zawiera Widget. Zaczynamy od stworzenia Widget, które chcemy, WidgetRepositoryaby powrócił. Następnie wykorzystujemy API Mockito do skonfigurowania WidgetRepository::findByIdmetody. Struktura naszej pozorowanej logiki jest następująca:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

W tym przypadku mówimy: Zwróć Optionalz naszego, Widgetgdy findById()metoda repozytorium zostanie wywołana z argumentem 1 (jako a long).

Następnie wywołujemy WidgetService„s findByIdmetody z argumentem 1. Następnie potwierdzić, że jest on obecny, a zwrócona Widgetjest ten, który mamy skonfigurowany makiety WidgetRepositorydo zwrotu.