WebTestClient in Spring Boot: Testing CRUD REST APIs

In this tutorial, we'll explore how to use Spring Boot's WebTestClient for testing CRUD (Create, Read, Update, Delete) operations in a RESTful service. WebTestClient is a non-blocking, reactive web client for testing web components.

What is WebTestClient?

WebTestClient is a client-side test tool that is part of the Spring WebFlux module. It's designed to test reactive and non-reactive web applications by performing requests and asserting responses without the need for running a server. WebTestClient is particularly useful for integration testing, where it can mimic the behavior of client requests and validate the responses from your RESTful services.

Key Features:

Non-Blocking Client: Suitable for testing reactive applications with asynchronous and event-driven behavior.

Fluent API: Offers a fluent API for building requests, sending them, and asserting responses.

Support for Both Web MVC and WebFlux: Works with both traditional servlet-based and reactive-based web applications.

Testing Spring Boot CRUD REST APIs using WebTestClient

In this tutorial, we'll create a Spring Boot application that performs CRUD operations on a User entity, using an H2 in-memory database for persistence. We'll then test these CRUD operations using WebTestClient. The application will be structured into three layers: Repository, Service, and Controller.

Project Setup

Ensure you have the following dependencies in your pom.xml:

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
    </dependency>
  

The User Entity

import jakarta.persistence.*;

    @Entity
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String firstName;
        private String lastName;
        private String email;
    
        // Constructors, Getters, Setters
    }

The UserRepository

import org.springframework.data.jpa.repository.JpaRepository;

    public interface UserRepository extends JpaRepository<User, Long> {
    }

The UserService

import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    import java.util.Optional;
    
    @Service
    public class UserService {
    
        private final UserRepository userRepository;
    
        @Autowired
        public UserService(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        public User createUser(User user) {
            return userRepository.save(user);
        }
    
        public Optional<User> getUser(Long id) {
            return userRepository.findById(id);
        }
    
        public User updateUser(Long id, User userDetails) {
            User user = userRepository.findById(id).orElseThrow();
            user.setFirstName(userDetails.getFirstName());
            user.setLastName(userDetails.getLastName());
            user.setEmail(userDetails.getEmail());
            return userRepository.save(user);
        }
    
        public void deleteUser(Long id) {
            userRepository.deleteById(id);
        }
    
        public List<User> getAllUsers() {
            return userRepository.findAll();
        }
    }

The UserController

import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        private final UserService userService;
    
        @Autowired
        public UserController(UserService userService) {
            this.userService = userService;
        }
    
        @PostMapping
        public ResponseEntity<User> createUser(@RequestBody User user) {
            return ResponseEntity.ok(userService.createUser(user));
        }
    
        @GetMapping("/{id}")
        public ResponseEntity<User> getUser(@PathVariable Long id) {
            return userService.getUser(id)
                    .map(ResponseEntity::ok)
                    .orElse(ResponseEntity.notFound().build());
        }
    
        @GetMapping
        public ResponseEntity<List<User>> getAllUsers() {
            return ResponseEntity.ok(userService.getAllUsers());
        }
    
        @PutMapping("/{id}")
        public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
            return ResponseEntity.ok(userService.updateUser(id, user));
        }
    
        @DeleteMapping("/{id}")
        public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
            userService.deleteUser(id);
            return ResponseEntity.ok().build();
        }
    }
    

Writing Tests with WebTestClient

First, configure WebTestClient in your test class:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @AutoConfigureWebTestClient
    public class UserControllerTest {
    
        @Autowired
        private WebTestClient webTestClient;
    
        // Test methods go here
    }

Now, let's write tests for each CRUD operation - create, retrieve, update, and delete User entities, and assert the responses using WebTestClient.

Preparing Test Data

For our test cases, we'll need a sample User object.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @AutoConfigureWebTestClient
    public class UserControllerTest {
    
        @Autowired
        private WebTestClient webTestClient;
    
        private User sampleUser;
    
        @BeforeEach
        void setUp() {
            sampleUser = new User();
            sampleUser.setFirstName("John");
            sampleUser.setLastName("Doe");
            sampleUser.setEmail("john.doe@example.com");
        }
    
        // Test methods will be added here
    }

Create (POST)

Testing the creation of a new User.

@Test
    public void createUserTest() {
        webTestClient.post()
                     .uri("/users")
                     .contentType(MediaType.APPLICATION_JSON)
                     .bodyValue(sampleUser)
                     .exchange()
                     .expectStatus().isOk()
                     .expectBody()
                     .jsonPath("$.firstName").isEqualTo("John")
                     .jsonPath("$.lastName").isEqualTo("Doe")
                     .jsonPath("$.email").isEqualTo("john.doe@example.com");
    }

Read (GET)

Testing retrieval of a User.

@Test
    public void getUserTest() {
        Long userId = 1L; // Assuming this ID exists in the database
        webTestClient.get()
                     .uri("/users/" + userId)
                     .exchange()
                     .expectStatus().isOk()
                     .expectBody()
                     .jsonPath("$.id").isEqualTo(userId)
                     .jsonPath("$.firstName").isEqualTo("John");
    }

Update (PUT)

Testing the update of a User.

@Test
    public void updateUserTest() {
        Long userId = 1L; // Assuming this ID exists
        User updatedUser = new User();
        updatedUser.setFirstName("Jane");
        updatedUser.setLastName("Doe");
        updatedUser.setEmail("jane.doe@example.com");
    
        webTestClient.put()
                     .uri("/users/" + userId)
                     .contentType(MediaType.APPLICATION_JSON)
                     .bodyValue(updatedUser)
                     .exchange()
                     .expectStatus().isOk()
                     .expectBody()
                     .jsonPath("$.firstName").isEqualTo("Jane")
                     .jsonPath("$.lastName").isEqualTo("Doe");
    }

Delete (DELETE)

Testing the deletion of a User.

@Test
    public void deleteUserTest() {
        Long userId = 1L; // Assuming this ID exists
        webTestClient.delete()
                     .uri("/users/" + userId)
                     .exchange()
                     .expectStatus().isOk();
    }

Conclusion

This tutorial covered creating a simple Spring Boot application with a User entity and performing CRUD operations using an H2 database. The application is structured into repository, service, and controller layers, and we tested these operations using WebTestClient, demonstrating the tool's effectiveness for testing web layers in Spring Boot applications.