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

When using JUnit 4, this annotation should be used in combination with @RunWith(SpringRunner.class). But for this example  we are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class).
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

Download the source code - click here!

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete