I recently started working on the functional approach of Spring Boot Webflux. You can explore it more on my previous blog on Spring Boot Webflux. It is a new concept and you may not find many useful blogs on it unlike for annotation based controllers. However, going with some trial and error, I have come out with how one can test its router functions along with its handler. We will we using WebTestClient and WebFluxTest here to do the testing.
To gain a better understanding of the topic, let’s try to understand it with the help of an example.
So, we have our configuration class for router functions, named as UserRouter.
@Configuration public class UserRouter { @Bean public RouterFunction<ServerResponse> userRoutes(UserHandler userHandler) { return RouterFunctions.route() .path("/users", builder -> builder .POST("", accept(MediaType.APPLICATION_JSON), userHandler::createUser) .GET("/{id}", accept(MediaType.APPLICATION_JSON), userHandler::getUser) .GET("", accept(MediaType.APPLICATION_JSON), userHandler::getUsers)) .build(); }
With this, we have defined a RouterHandler with the name UserHandler.
@Component @AllArgsConstructor public class UserHandler { private UserRepository userRepository; public Mono<ServerResponse> createUser(ServerRequest request) { Mono<User> user = request.bodyToMono(User.class); return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON) .body(fromPublisher(user.flatMap(userRepository::save), User.class)); } public Mono<ServerResponse> getUser(ServerRequest request) { final int id = Integer.parseInt(request.pathVariable("id")); final Mono<User> user = userRepository.findById(id); return user.flatMap(usr -> ok().contentType(APPLICATION_JSON) .body(fromPublisher(user, User.class))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> getUsers(ServerRequest request) { return ok().contentType(APPLICATION_JSON) .body(fromPublisher(userRepository.findAll(), User.class)); } }
To test our handlers from the perspective of a route as a whole, we will use the concept of WebTestClient and WebFluxTest.
@RunWith(SpringRunner.class) @ContextConfiguration(classes = {UserRouter.class, UserHandler.class}) @WebFluxTest public class TestEmployeeHandler { @Autowired private ApplicationContext context; @MockBean private UserRepository userRepository; private WebTestClient webTestClient; @Before public void setUp() { webTestClient = WebTestClient.bindToApplicationContext(context).build(); } @Test public void testGetUserById() { User user = User.builder().id(1).name("ABC").email("abc@xyz.com").build(); Mono<User> UserMono = Mono.just(user); when(userRepository.findById(1)).thenReturn(UserMono); webTestClient.get() .uri("/users/1") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody(User.class) .value(userResponse -> { Assertions.assertThat(userResponse.getId()).isEqualTo(1); Assertions.assertThat(userResponse.getName()).isEqualTo("ABC"); Assertions.assertThat(userResponse.getEmail()).isEqualTo("abc@xyz.com"); } ); } @Test public void testGetUsers() { User user1 = User.builder().id(1).name("ABC").email("abc@xyz.com").build(); User user2 = User.builder().id(2).name("XYZ").email("xyz@abc.com").build(); when(userRepository.findAll()).thenReturn(Flux.just(user1, user2)); webTestClient.get() .uri("/users") .exchange() .expectStatus().isOk() .expectBodyList(User.class) .value(userResponse -> { Assertions.assertThat(userResponse.get(0).getId()).isEqualTo(1); Assertions.assertThat(userResponse.get(0).getName()).isEqualTo("ABC"); Assertions.assertThat(userResponse.get(0).getEmail()).isEqualTo("abc@xyz.com"); Assertions.assertThat(userResponse.get(1).getId()).isEqualTo(2); Assertions.assertThat(userResponse.get(1).getName()).isEqualTo("XYZ"); Assertions.assertThat(userResponse.get(1).getEmail()).isEqualTo("xyz@abc.com"); } ); } @Test public void testCreateUser() { User user = User.builder().id(1).name("ABC").email("abc@xyz.com").build(); Mono<User> UserMono = Mono.just(user); when(userRepository.save(any())).thenReturn(UserMono); webTestClient.post() .uri("/users") .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .body(Mono.just(user), User.class) .exchange() .expectStatus().isOk() .expectBody(User.class) .value(userResponse -> { Assertions.assertThat(userResponse.getId()).isEqualTo(1); Assertions.assertThat(userResponse.getName()).isEqualTo("ABC"); Assertions.assertThat(userResponse.getEmail()).isEqualTo("abc@xyz.com"); } ); } }
To understand how the above code will work, let us try to understand some few concepts:
@ContextConfiguration: It defines class-level metadata that is used to determine how to load and configure an ApplicationContext for our test cases. Here, we have passed UserRouter and UserHandler to our context.
@WebFluxTest: Using this annotation will disable full auto-configuration and instead apply only configuration relevant to WebFlux tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, and WebFluxConfigurer beans but not @Component, @Service or @Repository beans).
We have used here @ContextConfiguration along with@WebFluxTest because right now, @WebFluxTest has no consistent way to detect RouterFunction beans, just like it does for the controller classes.
ApplicationContext: Inside our test class, we have created an instance of ApplicationContext, which is the central interface within a Spring application that is used for providing configuration information to the application.
WebTestClient: We have created an instance of WebTestClient and bound it with the context in the setup() method using
WebTestClient.bindToApplicationContext(context).build();
Now, we can just utilize our webTestClient instance to define the route and their expected response which are defined in our UserHandler. The assertions can be done using expectStatus(), expectHeader() or even expectBody(). How to use them is already given in the code above.
This was pretty much all, which I wanted to cover with this blog. The entire code can be found at the following GITHUB repository.
If you have any doubt or any suggestions to make please drop a comment. Thanks!
References:
Using @WebFluxTest to test RouterFunction
Reactive Test Support