Spring MVC е една от най-популярните Java рамки за изграждане на корпоративни Java приложения и се поддава много добре на тестване. По дизайн Spring MVC насърчава разделянето на проблемите и насърчава кодирането срещу интерфейси. Тези качества, заедно с внедряването от Spring на инжектиране на зависимости, правят приложенията на Spring много проверими.
Този урок е втората половина на моето въведение в модулното тестване с JUnit 5. Ще ви покажа как да интегрирате JUnit 5 с Spring, след което ще ви запозная с три инструмента, които можете да използвате за тестване на Spring MVC контролери, услуги и хранилища.
изтегляне Вземете кода Изтеглете изходния код за примерни приложения, използвани в този урок. Създадено от Стивън Хейнс за JavaWorld.Интегриране на JUnit 5 с Spring 5
За този урок използваме Maven и Spring Boot, така че първото нещо, което трябва да направим, е да добавим зависимостта JUnit 5 към нашия Maven POM файл:
org.junit.jupiter junit-jupiter 5.6.0 test
Точно както направихме в Част 1, ще използваме Mockito за този пример. И така, ще трябва да добавим библиотеката JUnit 5 Mockito:
org.mockito mockito-junit-jupiter 3.2.4 test
@ExtendWith и клас SpringExtension
JUnit 5 дефинира интерфейс за разширение , чрез който класовете могат да се интегрират с тестовете на JUnit на различни етапи от жизнения цикъл на изпълнение. Можем да активираме разширения, като добавим @ExtendWith
анотацията към нашите тестови класове и посочим класа на разширенията за зареждане. След това разширението може да реализира различни интерфейси за обратно извикване, които ще бъдат извикани през целия жизнен цикъл на теста: преди всички тестове да се изпълняват, преди всяко тестване, след всяко тестване и след като всички тестове са стартирани.
Spring определя SpringExtension
клас, който се абонира за известия за жизнения цикъл на JUnit 5, за да създаде и поддържа "тестов контекст". Спомнете си, че контекстът на приложението на Spring съдържа всички компоненти Spring в дадено приложение и че той извършва инжектиране на зависимости, за да свърже заедно приложение и неговите зависимости. Spring използва модела за разширение JUnit 5, за да поддържа контекста на приложението на теста, което прави писането на модулни тестове с Spring лесно.
След като добавихме библиотеката JUnit 5 към нашия Maven POM файл, можем да използваме, за SpringExtension.class
да разширим нашите JUnit 5 тестови класове:
@ExtendWith(SpringExtension.class) class MyTests { // ... }
Примерът, в този случай, е приложение Spring Boot. За щастие @SpringBootTest
анотацията вече включва @ExtendWith(SpringExtension.class)
анотацията, така че трябва само да я включим @SpringBootTest
.
Добавяне на зависимостта Mockito
За да тестваме правилно всеки компонент изолирано и да симулираме различни сценарии, ще искаме да създадем фиктивни реализации на зависимостите на всеки клас. Ето къде влиза Mockito. Включете следната зависимост във вашия POM файл, за да добавите поддръжка за Mockito:
org.mockito mockito-junit-jupiter 3.2.4 test
След като интегрирате JUnit 5 и Mockito във вашето приложение Spring, можете да използвате Mockito, като просто дефинирате Spring bean (като услуга или хранилище) във вашия тестов клас, използвайки @MockBean
анотацията. Ето нашия пример:
@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; ... }
В този пример ние създаваме макет WidgetRepository
в нашия WidgetServiceTest
клас. Когато Spring види това, той автоматично ще го свърже към нашия, WidgetService
за да можем да създадем различни сценарии в нашите тестови методи. Всеки метод за тестване ще конфигурира поведението на WidgetRepository
, например чрез връщане на заявеното Widget
или връщане на Optional.empty()
заявка, за която данните не са намерени. Ще прекараме останалата част от този урок, разглеждайки примери за различни начини за конфигуриране на тези фиктивни зърна.
Примерното приложение на Spring MVC
За да напишем модулни тестове на базата на Spring, имаме нужда от приложение, срещу което да ги запишем. За щастие можем да използваме примерното приложение от моя урок Spring Series „Овладяване на Spring framework 5, част 1: Spring MVC.“ Използвах примерното приложение от този урок като основно приложение. Модифицирах го с по-силен REST API, за да имаме още няколко неща за тестване.
Примерното приложение е уеб приложение на Spring MVC с контролер REST, сервизен слой и хранилище, което използва Spring Data JPA, за да продължи „джаджи“ към и от H2 база данни в паметта. Фигура 1 е общ преглед.

Какво е джаджа?
A Widget
е просто „нещо“ с идентификатор, име, описание и номер на версията. В този случай нашата джаджа е анотирана с JPA анотации, за да я дефинира като обект. Това WidgetRestController
е Spring MVC контролер, който преобразува RESTful API извиквания в действия за изпълнение Widgets
. Това WidgetService
е стандартна услуга Spring, която определя бизнес функционалността за Widgets
. И накрая, това WidgetRepository
е интерфейс Spring Data JPA, за който Spring ще създаде изпълнение по време на изпълнение. Ще прегледаме кода за всеки клас, докато пишем тестове в следващите раздели.
Тестване на пролетна услуга
Нека започнем с преглед на това как да тестваме услуга Spring , защото това е най-лесният компонент в нашето MVC приложение за тестване. Примерите в този раздел ще ни позволят да изследваме интеграцията на JUnit 5 с Spring, без да въвеждаме нови компоненти за тестване или библиотеки, въпреки че ще направим това по-късно в урока.
Ще започнем с преглед на WidgetService
интерфейса и WidgetServiceImpl
класа, които са показани съответно в Листинг 1 и Листинг 2.
Листинг 1. Интерфейсът на услугата 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); }
Листинг 2. Клас за изпълнение на услугата 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); } }
WidgetServiceImpl
е услуга Spring, коментирана с @Service
анотацията, която е WidgetRepository
свързана към нея чрез своя конструктор. На findById()
, findAll()
и deleteById()
методите са всички пропускателен методи спрямо базисното WidgetRepository
. Единствената бизнес логика, която ще намерите, се намира в save()
метода, който увеличава номера на версията, Widget
когато е запазен.
Тестовият клас
За да тестваме този клас, трябва да създадем и конфигурираме макет WidgetRepository
, да го свържем към WidgetServiceImpl
екземпляра и след това да го свържем WidgetServiceImpl
към нашия тестов клас. За щастие това е далеч по-лесно, отколкото звучи. Листинг 3 показва изходния код за WidgetServiceTest
класа.
Листинг 3. Тестовият клас на Spring service (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
Първият метод за изпитване, testFindById()
, изпълнява WidgetService
е findById()
метод, който трябва да се върне на Optional
която съдържа Widget
. Започваме със създаването на Widget
, което искаме WidgetRepository
да се върне. След това използваме Mockito API за конфигуриране на WidgetRepository::findById
метода. Структурата на нашата фиктивна логика е следната:
doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD
В този случай казваме: Върнете an Optional
от нашия, Widget
когато findById()
методът на хранилището бъде извикан с аргумент 1 (като a long
).
След това извикваме метода на WidgetService
' findById
с аргумент 1. След това потвърждаваме, че той присъства и че върнатият Widget
е този, който сме конфигурирали макетът WidgetRepository
да се връща.