Урок за JUnit 5, част 2: Тестване на единица Spring MVC с JUnit 5

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да се връща.