Spring-Webflux: Testing your Router Functions with WebTestClient

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

 

Knoldus-Scala-Spark-Services

Written by 

Vinisha Sharma is a software consultant having more than 6 months of experience. She thrives in a fast pace environment and loves exploring new technologies. She has experience with the languages such as C, C++, Java, Scala and is currently working on Java 8. Her hobbies include sketching and dancing. She believes Optimism and a learning attitude is the key to achieve success in your life

Leave a Reply

Knoldus Pune Careers - Hiring Freshers

Get a head start on your career at Knoldus. Join us!