优秀的编程知识分享平台

网站首页 > 技术文章 正文

Spring WebFlux 如何进行单元测试

nanyue 2024-10-17 11:15:31 技术文章 6 ℃

什么是单元测试?

单元测试是一种软件测试,重点验证软件应用程序的各个独立组件或单元的正确性。这些单元通常由代码库中的各个函数、方法或类组成。单元测试的主要目的是确保每个代码单元按预期运行并为给定的输入集生成正确的输出。

单元是测试驱动开发 (TDD) 方法的一个组成部分,开发人员在编写代码本身之前先编写测试。编写单元测试有助于设计具有清晰和模块化结构的软件,使其更易于维护,并随着时间的推移进行扩展。

什么是WebTestClient?

WebTestClient 是 Spring 框架测试基础设施的一部分,专门用于测试使用 Spring WebFlux 构建的应用程序,Spring WebFlux 是用于构建 Web 应用程序的响应式编程框架。可以使用 WebTestClient 为 Spring WebFlux 控制器和处理程序编写单元和集成测试。

单元测试

@WebFluxTest(controllers = TodoController.class)
@AutoConfigureWebTestClient
public class TodoControllerUnitTest {

    private final WebTestClient webTestClient;

    @MockBean
    private final TodoService todoService;

    private final String url = "/api/v1/todos";

    @Autowired
    public TodoControllerUnitTest(WebTestClient webTestClient, TodoService todoService) {
        this.webTestClient = webTestClient;
        this.todoService = todoService;
    }


                          .
                          .
                          .
                          .

    // 测试方法

}

@WebFluxTest(controllers = TodoController.class):

  • @WebFluxTest是一个 Spring Boot 测试注解,指示此类是对 WebFlux 控制器的测试。
  • controllers是一个属性,指定此测试重点关注哪个控制器。在本例中,它正在测试该类TodoController

@AutoConfigureWebTestClient:

  • @AutoConfigureWebTestClient是另一个 Spring Boot 测试注解,配置WebTestClient用于测试。WebTestClient是 Spring 提供的客户端,用于在测试期间向 Web 应用程序发出 HTTP 请求。
  • 它通常用于测试应用程序的端点、发送 HTTP 请求并验证响应。

@MockBean:

  • @MockBean用于创建 bean 的Mock以进行测试。在本例中,将创建TodoService bean 的Mock。
  • TodoService bean 通常是处理与待办事项相关的业务逻辑的服务,并且它测试时会被替换为Mock实例。这可以控制测试期间TodoService的行为。

1)测试findTodoById 方法

@GetMapping("/{id}")
@Operation(summary = "findTodoById")
Mono<ResponseEntity<Todo>> findTodoById(@PathVariable int id) {
    return todoService.findTodoById(id)
            .map(todo -> ResponseEntity.ok(todo));
}
@Test
void findTodoById_test_should_return_todo_object_and_status_should_be_ok() {
    var todo = Todo.builder().id(1).title("test title 1").description("test description 1").build();

    when(todoService.findTodoById(todo.getId())).thenReturn(Mono.just(todo));

    String findTodoByIdUrl = String.format("%s/%s", url, todo.getId());
    webTestClient
            .get()
            .uri(findTodoByIdUrl)
            .exchange()
            .expectStatus()
            .is2xxSuccessful()
            .expectBody(Todo.class)
            .consumeWith(result -> {
                var existTodo = result.getResponseBody();

                assert existTodo != null;
                assertEquals(todo.getId(), existTodo.getId());
                assertEquals(todo.getTitle(), existTodo.getTitle());

            });
}

@Test
void findTodoById_test_should_return_error_response_object_and_status_should_be_not_found() {
    var todo = Todo.builder().id(3).title("test title 3").description("test description 3").build();

    when(todoService.findTodoById(todo.getId())).thenReturn(Mono.error(new TodoNotFoundException(String.format("Todo not found. ID: %s", todo.getId()))));

    String findTodoByIdUrl = String.format("%s/%s", url, todo.getId());
    webTestClient
            .get()
            .uri(findTodoByIdUrl)
            .exchange()
            .expectStatus()
            .isNotFound()
            .expectBody(ErrorResponse.class)
            .consumeWith(result -> {
                var errorResponse = result.getResponseBody();

                assertEquals(false, errorResponse.isSuccess());
                assertEquals("Todo not found. ID: 3", errorResponse.getMessage());
            });
}

2)测试findAllTodos 方法

@GetMapping("")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "查找所有Todos")
Flux<Todo> findAllTodos() {
    return todoService.findAllTodos().log();
}
@Test
void findAllTodos_test_should_return_todo_list_and_status_should_be_ok() {
    var todos = List.of(
            Todo.builder().id(1).title("test title 1").description("test description 1").build(),
            Todo.builder().id(2).title("test title 2").description("test description 2").build(),
            Todo.builder().id(3).title("test title 3").description("test description 3").build()
    );

    when(todoService.findAllTodos()).thenReturn(Flux.fromIterable(todos));

    webTestClient
            .get()
            .uri(url)
            .exchange()
            .expectStatus()
            .is2xxSuccessful()
            .expectBodyList(Todo.class)
            .hasSize(3);
}

3)测试saveTodo方法

@PostMapping("")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "创建Todo")
Mono<ResponseEntity<Todo>> saveTodo(@RequestBody Todo todo) {
    return todoService.saveTodo(todo)
            .map(savedTodo -> ResponseEntity.status(HttpStatus.CREATED).body(savedTodo));
}
@Test
void saveTodo_test_should_return_saved_todo_and_status_should_be_created() {
    var todo = Todo.builder().id(1).title("test title 1").description("test description 1").build();

    when(todoService.saveTodo(isA(Todo.class))).thenReturn(Mono.just(todo));

    webTestClient
            .post()
            .uri(url)
            .bodyValue(todo)
            .exchange()
            .expectStatus()
            .isCreated()
            .expectBody(Todo.class)
            .consumeWith(result -> {
                var savedTodo = result.getResponseBody();

                assert savedTodo != null;
                assertEquals(todo.getId(), savedTodo.getId());
                assertEquals(todo.getTitle(), savedTodo.getTitle());

            });
}

4)测试deleteTodoById方法

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "根据id删除Todo")
Mono<Void> deleteTodoById(@PathVariable int id) {
    return todoService.deleteTodoById(id);
}
@Test
void deleteTodoById_test_should_return_empty_and_status_should_be_no_content() {
    int todoId = 3;

    when(todoService.deleteTodoById(todoId)).thenReturn(Mono.empty());

    String deleteUrl = String.format("%s/%s", url, todoId);
    webTestClient
            .delete()
            .uri(deleteUrl)
            .exchange()
            .expectStatus()
            .isNoContent();
}

运行所有测试并查看结果。

Tags:

最近发表
标签列表