用JUnit 5进行Spring Boot测试的详细指南

JUnit 5 (JUnit Jupiter)已经存在了相当长的一段时间,它配备了大量的功能,从Spring Boot 2.2开始, ,它是默认的测试库依赖。在这篇博文中,你会发现Spring Boot中的一些基本测试实例,以及针对基本Web应用的 。JUnit 5 JUnit 5

内容表

源代码

本文的源代码可以在Github上找到:https://github.com/kolorobot/spring-boot-junit5。

设置项目

Spring Boot 2.2增加了对JUnit Jupiter的默认支持。用Initializr (https://start.spring.io)生成的每个项目都有所有需要的依赖,生成的测试类使用@SpringBootTest 注释,该注解将测试配置为JUnit 5。

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

 @Test
 void contextLoads() {
 }

}

提示:如果你是JUnit 5的新手,请看我关于JUnit 5的其他帖子:https://blog.codeleak.pl/search/label/junit 5

运行测试

我们可以用Maven Wrapper:./mvnw clean testGradle Wrapper:./gradlew clean test 来运行测试。

带有单个REST控制器的示例应用程序

该示例应用程序包含一个具有三个端点的单一REST控制器:

  • /tasks/{id}
  • /tasks
  • /tasks?title={title}

每个控制器的方法都在内部调用JSONPlaceholder- 用于测试和原型设计的假的在线REST API。

项目文件的结构如下:

$ 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

它也有以下静态资源:

$ tree src/main/resources/
src/main/resources/
├── application.properties
├── static
│   ├── error
│   │   └── 404.html
│   └── index.html
└── templates

TaskController 将其工作委托给TaskRepository

@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) 端点:

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 绑定一些合理的属性:

@Configuration
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);
    }
}

注意:从Spring Boot 2.2开始,你不需要使用配置属性。@EnableConfigurationProperties

application.properties 包含与JSONPlaceholder端点配置有关的几个属性:

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测试的最常见情况。

在随机端口上运行Web服务器的Spring Boot测试

在下面的测试中,将使用一个随机端口创建Web环境。然后,这个端口被注入到用@LocalServerPort 注释的字段中。在这种模式下,应用程序使用嵌入式服务器执行:

@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测试中,Web服务器在随机端口上运行,并带有模拟的依赖性

如果你需要模拟任何Bean,你可以使用@MockBean 注解来标记任何依赖关系为模拟对象。Spring Boot使用Mockito创建模拟对象。在下面的例子中,应用程序将使用运行在默认端口的嵌入式服务器启动:

@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);
    }
}

带有模拟MVC层的Spring Boot测试

使用完全配置的嵌入式服务器来启动Spring Boot应用程序可能会很耗时,而且对于集成测试来说,这并不总是最好的选择。如果你在测试中不需要完整的服务器功能,你可以利用模拟的MVC层(MockMvc )。这可以通过添加@AutoConfigureMockMvc@SpringBootTest 来实现:

@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}"));
    }
}

带有模拟MVC层和模拟依赖的Spring Boot测试

@MockedBean 可以与自动配置的 MockMvc

@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}"));
    }
}

带有模拟Web层的Spring Boot测试

如果只需要Web层(而不是上下文配置),你可以使用@WebMvcTest

@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}"));
    }
}

带有模拟Web层和模拟依赖的Spring Boot测试

@MockedBean 可以与 一起使用@WebMvcTest

@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 testGradle Wrapper:./gradlew clean test 来运行所有测试。

使用Gradle 运行测试的结果:

$ ./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

参考文献

也请参见

源代码

本文的源代码可以在Github上找到:https://github.com/kolorobot/spring-boot-junit5。

猜你喜欢

转载自juejin.im/post/7126035708061417509