JUnit 5
(JUnit Jupiter) существует уже довольно давно и оснащен множеством функций. Но, что удивительно, JUnit 5
не является зависимостью библиотеки тестов по умолчанию, когда дело доходит до Spring Boot Test Starter: это все еще JUnit 4.12
, выпущенный еще в 2014 году. Если вы JUnit 5
использовать JUnit 5
в своем следующем проекте на основе Spring Boot, тогда этот пост в блоге для вас. Вы узнаете об основной настройке для проектов на базе Gradle
и Maven
с примерами тестов Spring Boot для различных вариантов использования.
Исходный код
Исходный код этой статьи можно найти на Github: https://github.com/kolorobot/spring-boot-junit5 .
Настройте проект с нуля
Для настройки проекта вам понадобится JDK 11 или более поздняя версия и Gradle или Maven (в зависимости от ваших предпочтений). Самый простой способ начать работу с Spring Boot — использовать Initializr по адресу https://start.spring.io . Единственные зависимости для выбора — Spring Web
. Тестирование зависимостей ( Spring Boot Starter Test
) всегда включено, независимо от того, какие зависимости вы используете в сгенерированном проекте.
Сборка с Gradle
Файл проекта по умолчанию для сборки Gradle ( gradle.build
), созданный с помощью Initializr
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
plugins { id 'org.springframework.boot' version '2.1.8.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' id 'java' } group = 'pl.codeleak.samples' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' } |
Чтобы добавить поддержку JUnit 5
нам нужно исключить старую зависимость JUnit 4
и включить зависимость JUnit 5
(JUnit Jupiter):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation( 'org.springframework.boot:spring-boot-starter-test' ) { exclude group: 'junit' , module: 'junit' } testCompile 'org.junit.jupiter:junit-jupiter:5.5.2' } test { useJUnitPlatform() testLogging { events "passed" , "skipped" , "failed" } } |
Сборка с Maven
Файл проекта по умолчанию для сборки Maven ( pom.xml
), созданный с помощью Initializr
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
<? xml version = "1.0" encoding = "UTF-8" ?> < project > < modelVersion >4.0.0</ modelVersion > < parent > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-parent</ artifactId > < version >2.1.8.RELEASE</ version > < relativePath /> <!-- lookup parent from repository --> </ parent > < groupId >pl.codeleak.samples</ groupId > < artifactId >spring-boot-junit5</ artifactId > < version >0.0.1-SNAPSHOT</ version > < name >spring-boot-junit5</ name > < description >Demo project for Spring Boot and JUnit 5</ description > < properties > < project.build.sourceEncoding >UTF-8</ project.build.sourceEncoding > < java.version >11</ java.version > </ properties > < dependencies > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-test</ artifactId > < scope >test</ scope > </ dependency > </ dependencies > < build > < plugins > < plugin > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-maven-plugin</ artifactId > </ plugin > </ plugins > </ build > </ project > |
Чтобы добавить поддержку JUnit 5
нам нужно исключить старую зависимость JUnit 4
и включить зависимость JUnit 5
(JUnit Jupiter):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
< properties > < junit.jupiter.version >5.5.2</ junit.jupiter.version > </ properties > < dependencies > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-test</ artifactId > < scope >test</ scope > < exclusions > < exclusion > < groupId >junit</ groupId > < artifactId >junit</ artifactId > </ exclusion > </ exclusions > </ dependency > < dependency > < groupId >org.junit.jupiter</ groupId > < artifactId >junit-jupiter</ artifactId > < version >${junit.jupiter.version}</ version > < scope >test</ scope > </ dependency > </ dependencies > |
Используйте JUnit 5 в тестовом классе
Тест, сгенерированный Initializr
содержит автоматически сгенерированный тест JUnit 4
. Чтобы применить JUnit 5
нам нужно изменить импорт и заменить бегун JUnit 5
расширением JUnit 5
. Мы также можем сделать класс и пакет тестовых методов защищенными:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; @ExtendWith (SpringExtension. class ) @SpringBootTest class SpringBootJunit5ApplicationTests { @Test void contextLoads() { } } |
Совет: Если вы новичок в JUnit 5, посмотрите мои другие посты о JUnit 5: https://blog.codeleak.pl/search/label/junit 5
Запустить тест
Мы можем запустить тест либо с помощью Maven Wrapper
: ./mvnw clean test
либо с помощью Gradle Wrapper
: ./gradlew clean test
.
Исходный код
Пожалуйста, ознакомьтесь с этим коммитом для изменений, связанных с настройкой проекта
Пример приложения с одним контроллером REST
Пример приложения содержит один контроллер REST с тремя конечными точками:
-
/tasks/{id}
-
/tasks
-
/tasks?title={title}
Каждый из методов контроллера вызывает внутренний JSONPlaceholder — фальшивый онлайн REST API для тестирования и создания прототипов.
Структура файлов проекта выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
$ tree src/main/java src/main/java └── pl └── codeleak └── samples └── springbootjunit5 ├── SpringBootJunit5Application.java ├── config │ ├── JsonPlaceholderApiConfig.java │ └── JsonPlaceholderApiConfigProperties.java └── todo ├── JsonPlaceholderTaskRepository.java ├── Task.java ├── TaskController.java └── TaskRepository.java |
Он также имеет следующие статические ресурсы:
1
2
3
4
5
6
7
8
|
$ tree src/main/resources/ src/main/resources/ ├── application.properties ├── static │ ├── error │ │ └── 404 .html │ └── index.html └── templates |
TaskController
делегирует свою работу в TaskRepository
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@RestController class TaskController { private final TaskRepository taskRepository; TaskController(TaskRepository taskRepository) { this .taskRepository = taskRepository; } @GetMapping ( "/tasks/{id}" ) Task findOne( @PathVariable Integer id) { return taskRepository.findOne(id); } @GetMapping ( "/tasks" ) List<Task> findAll() { return taskRepository.findAll(); } @GetMapping (value = "/tasks" , params = "title" ) List<Task> findByTitle(String title) { return taskRepository.findByTitle(title); } } |
TaskRepository
реализуется JsonPlaceholderTaskRepository
который внутренне использует RestTemplate
для вызова конечной точки JSONPlaceholder ( https://jsonplaceholder.typicode.com ):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
public class JsonPlaceholderTaskRepository implements TaskRepository { private final RestTemplate restTemplate; private final JsonPlaceholderApiConfigProperties properties; public JsonPlaceholderTaskRepository(RestTemplate restTemplate, JsonPlaceholderApiConfigProperties properties) { this .restTemplate = restTemplate; this .properties = properties; } @Override public Task findOne(Integer id) { return restTemplate .getForObject( "/todos/{id}" , Task. class , id); } // other methods skipped for readability } |
Приложение настраивается через JsonPlaceholderApiConfig
который использует JsonPlaceholderApiConfigProperties
для привязки некоторых разумных свойств из application.properties
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Configuration @EnableConfigurationProperties (JsonPlaceholderApiConfigProperties. class ) public class JsonPlaceholderApiConfig { private final JsonPlaceholderApiConfigProperties properties; public JsonPlaceholderApiConfig(JsonPlaceholderApiConfigProperties properties) { this .properties = properties; } @Bean RestTemplate restTemplate() { return new RestTemplateBuilder() .rootUri(properties.getRootUri()) .build(); } @Bean TaskRepository taskRepository(RestTemplate restTemplate, JsonPlaceholderApiConfigProperties properties) { return new JsonPlaceholderTaskRepository(restTemplate, properties); } } |
application.properties
содержит несколько свойств, связанных с конфигурацией конечной точки JSONPlaceholder:
1
2
3
4
|
json-placeholder.root-uri=https: //jsonplaceholder.typicode.com json-placeholder.todo-find-all.sort=id json-placeholder.todo-find-all.order=desc json-placeholder.todo-find-all.limit= 20 |
Подробнее о @ConfigurationProperties
в этом сообщении в блоге: https://blog.codeleak.pl/2014/09/using-configurationproperties-in-spring.html.
Исходный код
Пожалуйста, обратитесь к этой фиксации для изменений, связанных с исходным кодом приложения.
Создание тестов Spring Boot
Spring Boot предоставляет ряд утилит и аннотаций, которые поддерживают тестирование приложений.
При создании тестов можно использовать разные подходы. Ниже вы найдете наиболее распространенные случаи создания тестов Spring Boot.
Тест Spring Boot с веб-сервером, работающим на случайном порте
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
@ExtendWith (SpringExtension. class ) @SpringBootTest (webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class TaskControllerIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void findsTaskById() { // act // assert assertThat(task) .extracting(Task::getId, Task::getTitle, Task::isCompleted, Task::getUserId) .containsExactly( 1 , "delectus aut autem" , false , 1 ); } } |
Тест Spring Boot с веб-сервером, работающим на случайном порте с поддельной зависимостью
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@ExtendWith (SpringExtension. class ) @SpringBootTest (webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class TaskControllerIntegrationTestWithMockBeanTest { @LocalServerPort private int port; @MockBean private TaskRepository taskRepository; @Autowired private TestRestTemplate restTemplate; @Test void findsTaskById() { // arrange var taskToReturn = new Task(); taskToReturn.setId( 1 ); taskToReturn.setTitle( "delectus aut autem" ); taskToReturn.setCompleted( true ); taskToReturn.setUserId( 1 ); when(taskRepository.findOne( 1 )).thenReturn(taskToReturn); // act // assert assertThat(task) .extracting(Task::getId, Task::getTitle, Task::isCompleted, Task::getUserId) .containsExactly( 1 , "delectus aut autem" , true , 1 ); } } |
Тест Spring Boot с имитированным слоем MVC
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@ExtendWith (SpringExtension. class ) @SpringBootTest @AutoConfigureMockMvc class TaskControllerMockMvcTest { @Autowired private MockMvc mockMvc; @Test void findsTaskById() throws Exception { mockMvc.perform(get( "/tasks/1" )) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().json( "{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":false}" )); } } |
Spring Boot test с поддельным слоем MVC и поддельной зависимостью
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@ExtendWith (SpringExtension. class ) @SpringBootTest @AutoConfigureMockMvc class TaskControllerMockMvcWithMockBeanTest { @Autowired private MockMvc mockMvc; @MockBean private TaskRepository taskRepository; @Test void findsTaskById() throws Exception { // arrange var taskToReturn = new Task(); taskToReturn.setId( 1 ); taskToReturn.setTitle( "delectus aut autem" ); taskToReturn.setCompleted( true ); taskToReturn.setUserId( 1 ); when(taskRepository.findOne( 1 )).thenReturn(taskToReturn); // act and assert mockMvc.perform(get( "/tasks/1" )) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().json( "{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":true}" )); } } |
Spring Boot test с надломанным веб-слоем
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@ExtendWith (SpringExtension. class ) @WebMvcTest @Import (JsonPlaceholderApiConfig. class ) class TaskControllerWebMvcTest { @Autowired private MockMvc mockMvc; @Test void findsTaskById() throws Exception { mockMvc.perform(get( "/tasks/1" )) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().json( "{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":false}" )); } } |
Spring Boot test с поддельным веб-слоем и поддельной зависимостью
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@ExtendWith (SpringExtension. class ) @WebMvcTest class TaskControllerWebMvcWithMockBeanTest { @Autowired private MockMvc mockMvc; @MockBean private TaskRepository taskRepository; @Test void findsTaskById() throws Exception { // arrange var taskToReturn = new Task(); taskToReturn.setId( 1 ); taskToReturn.setTitle( "delectus aut autem" ); taskToReturn.setCompleted( true ); taskToReturn.setUserId( 1 ); when(taskRepository.findOne( 1 )).thenReturn(taskToReturn); // act and assert mockMvc.perform(get( "/tasks/1" )) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().json( "{\"id\":1,\"title\":\"delectus aut autem\",\"userId\":1,\"completed\":true}" )); } } |
Запустите все тесты
Мы можем запустить все тесты либо с помощью Maven Wrapper
: ./mvnw clean test
либо с помощью Gradle Wrapper
: ./gradlew clean test
.
Результаты выполнения тестов с Gradle
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
$ ./gradlew clean test > Task :test pl.codeleak.samples.springbootjunit5.SpringBootJunit5ApplicationTests > contextLoads() PASSED pl.codeleak.samples.springbootjunit5.todo.TaskControllerWebMvcTest > findsTaskById() PASSED pl.codeleak.samples.springbootjunit5.todo.TaskControllerIntegrationTestWithMockBeanTest > findsTaskById() PASSED pl.codeleak.samples.springbootjunit5.todo.TaskControllerWebMvcWithMockBeanTest > findsTaskById() PASSED pl.codeleak.samples.springbootjunit5.todo.TaskControllerIntegrationTest > findsTaskById() PASSED pl.codeleak.samples.springbootjunit5.todo.TaskControllerMockMvcTest > findsTaskById() PASSED pl.codeleak.samples.springbootjunit5.todo.TaskControllerMockMvcWithMockBeanTest > findsTaskById() PASSED BUILD SUCCESSFUL in 7s 5 actionable tasks: 5 executed |
Рекомендации
- https://docs.spring.io/spring-boot/docs/2.1.8.RELEASE/reference/html/boot-features-testing.html
- https://spring.io/guides/gs/testing-web/
- https://github.com/spring-projects/spring-boot/issues/14736
См. Оригинальную статью здесь: тестирование Spring Boot с помощью JUnit 5 Мнения, высказанные участниками Java Code Geeks, являются их собственными. |