Spring Boot - Testing Spring WebFlux Controller with @WebFluxTest - Example
In this section, we will learn how to test Spring WebFlux Controller with @WebFluxTest.
1. @WebFluxTest
Instead of bootstrapping the entire application context for every test, @WebFluxTest allows us to initialize only the parts of the Application context that are necessary for our Spring WebFlux layer. This allows us to focus on testing the controllers, and related components.
Following beans will be scanned while using @WebFluxTest:
- @Controller
- @ControllerAdvice
- @JsonComponent
- Converter
- GenericConverter
- Filter
- WebFluxConfigurer
Regular @Component, @Service or @Repository beans are not scanned when using this annotation.
This approach not only speeds up the testing process but also ensures a focused and efficient testing environment.
This approach is also known as "slicing" the application context.
The annotation can be used to test a single controller by passing it as an attribute, for example @WebFluxTest(SomeController.class).
@WebFluxTest(StudentController.class)
public class StudentControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private StudentService studentService;
//Unit test Save Student REST API
@Test
void shouldCreateStudent() throws Exception {
//todo...
}
}
- We use @MockBean in Spring Boot when we want to mock an object that is present in the Spring application context.@MockBean takes care of replacing the bean with what we want to simulate in our test.
- WebTestClient supports execution of tests against Spring WebFlux server endpoints. WebTestClient uses simulated requests and responses to avoid exhausting server resources and can bind directly to the WebFlux server infrastructure. WebTestClient is similar to MockMvc. The only difference between those test web clients is that WebTestClient is aimed at testing WebFlux endpoints.
2. Creating a spring boot application
First, open the Spring initializr https://start.spring.io
Then, Provide the Group and Artifact name. We have provided Group name com.knf.dev.demo and Artifact webflux-example. Here I selected the Maven project - language Java 17 - Spring Boot 3.1.5 , Spring Reactive Web, Spring Data R2DBC, and H2 Database.
Then, click on the Generate button. When we click on the Generate button, it starts packing the project in a .zip(webfluxtest-example) file and downloads the project. Then, Extract the Zip file.
Then, import the project on your favourite IDE.
Final Project Directory:
Complete pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>webfluxtest-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>webfluxtest-example</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring-boot-starter-test starter will provide following libraries:
- JUnit
- Spring Test & Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
Create schema.sql
CREATE TABLE IF NOT EXISTS students
(id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255),
PRIMARY KEY (id));
Create Student Entity Class
package com.knf.dev.demo.entity;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.annotation.Id;
@Table("students")
public class Student {
@Id
private Integer id;
private String name;
private String email;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Student(String name, String email) {
this.name = name;
this.email = email;
}
public Student() {
}
public Student(Integer id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}
Create Student Repository
package com.knf.dev.demo.repository;
import com.knf.dev.demo.entity.Student;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
public interface StudentRepository extends ReactiveCrudRepository<Student, Integer> {
}
Create Student Service
package com.knf.dev.demo.service;
import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.repository.StudentRepository;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Flux<Student> getAllStudents(){
return this.studentRepository.findAll();
}
public Mono<Student> getStudentById(int id){
return this.studentRepository.findById(id);
}
public Mono<Student> createStudent(final Student student){
return this.studentRepository.save(student);
}
public Mono<Student> updateStudent(int id, final Mono<Student> studentMono){
return this.studentRepository.findById(id)
.flatMap(p -> studentMono.map(u -> {
p.setEmail(u.getEmail());
p.setName(u.getName());
return p;
}))
.flatMap(p -> this.studentRepository.save(p));
}
public Mono<Void> deleteStudent(final int id){
return this.studentRepository.deleteById(id);
}
}
Create Student Controller
package com.knf.dev.demo.controller;
import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.service.StudentService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/v1")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@GetMapping("/students")
public Flux<Student> getAllStudents(){
return this.studentService.getAllStudents();
}
@GetMapping("/students/{id}")
public Mono<ResponseEntity<Student>> getStudentById(@PathVariable int id){
return this.studentService.getStudentById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping("/students")
public Mono<Student> createStudent(@RequestBody Mono<Student> studentMono){
return studentMono.flatMap(this.studentService::createStudent);
}
@PutMapping("/students/{id}")
public Mono<Student> updateStudent(@PathVariable int id,
@RequestBody Mono<Student> studentMono){
return this.studentService.updateStudent(id, studentMono);
}
@DeleteMapping("/students/{id}")
public Mono<Void> deleteStudent(@PathVariable int id){
return this.studentService.deleteStudent(id);
}
}
WebfluxtestExampleApplication class
package com.knf.dev.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebfluxtestExampleApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxtestExampleApplication.class, args);
}
}
Write Unit test for StudentController
Let’s write our test cases by creating StudentControllerTest class
Create StudentControllerTest
package com.knf.dev.demo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.knf.dev.demo.controller.StudentController;
import com.knf.dev.demo.entity.Student;
import com.knf.dev.demo.service.StudentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@WebFluxTest(StudentController.class)
public class StudentControllerTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private StudentService studentService;
//Unit test Save Student REST API
@Test
void shouldCreateStudent() throws Exception {
// Setup
Student student = new Student(1,"Alpha","alpha@tmail.com");
given(studentService.createStudent(any(Student.class)))
.willReturn(Mono.just(student));
// Action that we are going test
WebTestClient.ResponseSpec response = webTestClient.post()
.uri("/api/v1/students")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(student), Student.class)
.exchange();
// Verify the result using assert statements
response.expectStatus().is2xxSuccessful()
.expectBody()
.consumeWith(System.out::println)
.jsonPath("$.name").isEqualTo(student.getName())
.jsonPath("$.email").isEqualTo(student.getEmail());
}
// Unit test Get Student by Id REST API
@Test
public void shouldReturnStudent() {
// Setup
Integer id = 1;
Student student = new Student("Alpha","alpha@tmail.com");
given(studentService.getStudentById(id)).willReturn(Mono.just(student));
// Action that we are going test
WebTestClient.ResponseSpec response = webTestClient.get()
.uri("/api/v1/students/{id}", Collections.singletonMap("id", id))
.exchange();
// Verify the result using assert statements
response.expectStatus().isOk()
.expectBody()
.consumeWith(System.out::println)
.jsonPath("$.name").isEqualTo(student.getName())
.jsonPath("$.email").isEqualTo(student.getEmail());
}
// Unit test Get All Student REST API
@Test
public void shouldReturnListOfStudents() {
// Setup
List<Student> students = new ArrayList<>(
Arrays.asList(new Student(1, "Alpha", "alpha@tmail.com"),
new Student(2, "Beta", "beta@tmail.com"),
new Student(3, "Gama", "gama@tmail.com")));
Flux<Student> studentFlux = Flux.fromIterable(students);
given(studentService.getAllStudents()).willReturn(studentFlux);
// Action that we are going test
WebTestClient.ResponseSpec response = webTestClient.get().uri("/api/v1/students")
.accept(MediaType.APPLICATION_JSON)
.exchange();
// Verify the result using assert statements
response.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(Student.class)
.consumeWith(System.out::println);
}
@Test
public void shouldUpdateStudent() throws Exception {
// Setup
Integer id = 1;
Student student = new Student("Alpha","alpha@tmail.com");
// Action that we are going test
given(studentService.updateStudent(any(Integer.class),any(Mono.class)))
.willReturn(Mono.just(student));
WebTestClient.ResponseSpec response = webTestClient.put()
.uri("/api/v1/students/{id}", Collections.singletonMap("id", id))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(student), Student.class)
.exchange();
// Verify the result using assert statements
response.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.consumeWith(System.out::println)
.jsonPath("$.name").isEqualTo(student.getName())
.jsonPath("$.email").isEqualTo(student.getEmail());
}
// Unit Test Delete Student REST API
@Test
public void shouldDeleteStudent() {
// Setup
Integer id = 1;
Mono<Void> voidReturn = Mono.empty();
given(studentService.deleteStudent(id)).willReturn(voidReturn);
// Action that we are going test
WebTestClient.ResponseSpec response = webTestClient.delete()
.uri("/api/v1/students/{id}", Collections.singletonMap("id", id))
.exchange();
// Verify the result using assert statements
response.expectStatus().is2xxSuccessful()
.expectBody()
.consumeWith(System.out::println);
}
}
- We use @MockBean in Spring Boot when we want to mock an object that is present in the Spring application context.@MockBean takes care of replacing the bean with what we want to simulate in our test.
- WebTestClient supports execution of tests against Spring WebFlux server endpoints. WebTestClient uses simulated requests and responses to avoid exhausting server resources and can bind directly to the WebFlux server infrastructure. WebTestClient is similar to MockMvc. The only difference between those test web clients is that WebTestClient is aimed at testing WebFlux endpoints.
3. Run the test
Or you can run the test using following command:
mvn test -Dtest=StudentControllerTest