Spring – @WebFluxTest with WebTestClient

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

1. @WebFluxTest with WebTestClient

1.1. Maven dependencies

Adding the reactive-test dependency.

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

1.2. @WebFluxTest annotation

It desables full auto-configuration and instead apply only 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 non-blocking, reactive client for testing web servers which 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 the need for 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 webflux controllers

2.1. Junit 5 tests for webflux controller

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

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.

2.2. Spring boot webflux controller under test

For reference, let’s see the controller which has been tested above.

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

Drop me your questions related to unit test spring webflux controller using @WebFluxTest and WebTestClient.

Happy Learning !!

Was this post helpful?

Join 7000+ Fellow Programmers

Subscribe to get new post notifications, industry updates, best practices, and much more. Directly into your inbox, for free.

1 thought on “Spring – @WebFluxTest with WebTestClient”

  1. how to do the same testing if WebFlux RouterFunction is used instead of RestController. How to tell to the test that it is WebFlux test?

    Reply

Leave a Comment

HowToDoInJava

A blog about Java and its related technologies, the best practices, algorithms, interview questions, scripting languages, and Python.