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)@SpringBootTestclass 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/javasrc/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
|
@RestControllerclass 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.comjson-placeholder.todo-find-all.sort=idjson-placeholder.todo-find-all.order=descjson-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@AutoConfigureMockMvcclass 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@AutoConfigureMockMvcclass 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)@WebMvcTestclass 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 :testpl.codeleak.samples.springbootjunit5.SpringBootJunit5ApplicationTests > contextLoads() PASSEDpl.codeleak.samples.springbootjunit5.todo.TaskControllerWebMvcTest > findsTaskById() PASSEDpl.codeleak.samples.springbootjunit5.todo.TaskControllerIntegrationTestWithMockBeanTest > findsTaskById() PASSEDpl.codeleak.samples.springbootjunit5.todo.TaskControllerWebMvcWithMockBeanTest > findsTaskById() PASSEDpl.codeleak.samples.springbootjunit5.todo.TaskControllerIntegrationTest > findsTaskById() PASSEDpl.codeleak.samples.springbootjunit5.todo.TaskControllerMockMvcTest > findsTaskById() PASSEDpl.codeleak.samples.springbootjunit5.todo.TaskControllerMockMvcWithMockBeanTest > findsTaskById() PASSEDBUILD SUCCESSFUL in 7s5 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, являются их собственными. |