Spring Boot @WebFluxTest and WebTestClient with JUnit 5

Learn to unit test Spring boot webflux controller using @WebFluxTest annotation and WebTestClient which is used to test webflux endpoints with Junit 5.

1. Configuring @WebFluxTest with WebTestClient

1.1. Maven

Start with adding the latest version of reactive-test dependency.

<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-test</artifactId>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

1.2. @WebFluxTest

It disables full auto-configuration and only applies configuration relevant to WebFlux tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter and WebFluxConfigurer beans but NOT @Component, @Service or @Repository beans).

By default, tests annotated with @WebFluxTest will also auto-configure a WebTestClient.

Typically @WebFluxTest is used in combination with @MockBean or @Import to create any collaborators required by the @Controller beans.

To write integration tests which require full application context – consider using @SpringBootTest combined with @AutoConfigureWebTestClient.

1.3. WebTestClient

It is a non-blocking, reactive client for testing web servers that uses the reactive WebClient internally to perform requests and provides a fluent API to verify responses.

It can connect to any server over an HTTP, or bind directly to WebFlux applications using mock request and response objects, without needing an HTTP server.

WebTestClient is similar to MockMvc. The only difference between those test web clients is that WebTestClient is aimed at testing WebFlux endpoints.

2. Testing Async Controller

2.1. System Under Test

In the given example, we are testing the EmployeeController class, which contains reactive methods for CRUD operations.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.howtodoinjava.demo.model.Employee;
import com.howtodoinjava.demo.service.EmployeeService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
  
@RestController
public class EmployeeController 
{
    @Autowired
    private EmployeeService employeeService;
  
    @PostMapping(value = { "/create", "/" })
    @ResponseStatus(HttpStatus.CREATED)
    public void create(@RequestBody Employee e) {
        employeeService.create(e);
    }
  
    @GetMapping(value = "/{id}")
    @ResponseStatus(HttpStatus.OK)
    public ResponseEntity<Mono<Employee>> findById(@PathVariable("id") Integer id) {
        Mono<Employee> e = employeeService.findById(id);
        HttpStatus status = (e != null) ? HttpStatus.OK : HttpStatus.NOT_FOUND;
        return new ResponseEntity<>(e, status);
    }
  
    @GetMapping(value = "/name/{name}")
    @ResponseStatus(HttpStatus.OK)
    public Flux<Employee> findByName(@PathVariable("name") String name) {
        return employeeService.findByName(name);
    }
  
    @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    @ResponseStatus(HttpStatus.OK)
    public Flux<Employee> findAll() {
      return employeeService.findAll();
    }
  
    @PutMapping(value = "/update")
    @ResponseStatus(HttpStatus.OK)
    public Mono<Employee> update(@RequestBody Employee e) {
        return employeeService.update(e);
    }
  
    @DeleteMapping(value = "/delete/{id}")
    @ResponseStatus(HttpStatus.OK)
    public void delete(@PathVariable("id") Integer id) {
        employeeService.delete(id).subscribe();
    }
}

2.2. JUnit Tests

The following class contains the JUnit tests that verify the async API handler methods, status and responses.

import static org.mockito.Mockito.times;

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
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.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.BodyInserters;
import com.howtodoinjava.demo.controller.EmployeeController;
import com.howtodoinjava.demo.dao.EmployeeRepository;
import com.howtodoinjava.demo.model.Employee;
import com.howtodoinjava.demo.service.EmployeeService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
 
@ExtendWith(SpringExtension.class)
@WebFluxTest(controllers = EmployeeController.class)
@Import(EmployeeService.class)
public class EmployeeControllerTest 
{
  @MockBean
  EmployeeRepository repository;
 
  @Autowired
  private WebTestClient webClient;
 
  @Test
  void testCreateEmployee() {
    Employee employee = new Employee();
    employee.setId(1);
    employee.setName("Test");
    employee.setSalary(1000);
 
    Mockito.when(repository.save(employee)).thenReturn(Mono.just(employee));
 
    webClient.post()
      .uri("/create")
      .contentType(MediaType.APPLICATION_JSON)
      .body(BodyInserters.fromObject(employee))
      .exchange()
      .expectStatus().isCreated();
 
    Mockito.verify(repository, times(1)).save(employee);
  }
   
  @Test
    void testGetEmployeesByName() 
  {
    Employee employee = new Employee();
    employee.setId(1);
    employee.setName("Test");
    employee.setSalary(1000);
     
    List<Employee> list = new ArrayList<Employee>();
    list.add(employee);
     
    Flux<Employee> employeeFlux = Flux.fromIterable(list);
     
        Mockito
            .when(repository.findByName("Test"))
            .thenReturn(employeeFlux);
 
        webClient.get().uri("/name/{name}", "Test")
          .header(HttpHeaders.ACCEPT, "application/json")
          .exchange()
          .expectStatus().isOk()
          .expectBodyList(Employee.class);
         
        Mockito.verify(repository, times(1)).findByName("Test");
    }
   
  @Test
    void testGetEmployeeById() 
  {
    Employee employee = new Employee();
    employee.setId(100);
    employee.setName("Test");
    employee.setSalary(1000);
       
        Mockito
            .when(repository.findById(100))
            .thenReturn(Mono.just(employee));
 
        webClient.get().uri("/{id}", 100)
          .exchange()
          .expectStatus().isOk()
          .expectBody()
          .jsonPath("$.name").isNotEmpty()
          .jsonPath("$.id").isEqualTo(100)
          .jsonPath("$.name").isEqualTo("Test")
          .jsonPath("$.salary").isEqualTo(1000);
         
        Mockito.verify(repository, times(1)).findById(100);
    }
 
  @Test
    void testDeleteEmployee() 
  {
    Mono<Void> voidReturn  = Mono.empty();
        Mockito
            .when(repository.deleteById(1))
            .thenReturn(voidReturn);
 
        webClient.get().uri("/delete/{id}", 1)
          .exchange()
          .expectStatus().isOk();
    }
}
  • We are using @ExtendWith( SpringExtension.class ) to support testing in Junit 5. In Junit 4, we need to use @RunWith(SpringRunner.class).
  • We used @Import(EmployeeService.class) to provide service dependency to application context which is not automatically scanned while using @WebFluxTest.
  • We have mocked the EmployeeRepository which is of type ReactiveMongoRepository. This will prevent the actual DB insertions and updates.
  • WebTestClient is used to hit particular endpoints of the controller and verify whether it returns the correct status codes and body.

Drop me your questions about the unit test spring webflux controller using @WebFluxTest and WebTestClient.

Happy Learning !!

Sourcecode On Github

Comments

Subscribe
Notify of
guest
3 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode