Статьи

Весеннее тестирование загрузки с JUnit 5

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
        var task = restTemplate.getForObject("http://localhost:" + port + "/tasks/1", Task.class);
 
        // 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
        var task = restTemplate.getForObject("http://localhost:" + port + "/tasks/1", Task.class);
 
        // 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

Рекомендации

См. Оригинальную статью здесь: тестирование Spring Boot с помощью JUnit 5

Мнения, высказанные участниками Java Code Geeks, являются их собственными.