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.

Comments