diff --git a/spring-boot-modules/spring-boot-4/pom.xml b/spring-boot-modules/spring-boot-4/pom.xml
index 1fa59f2fb97e..4e2b475d3b2c 100644
--- a/spring-boot-modules/spring-boot-4/pom.xml
+++ b/spring-boot-modules/spring-boot-4/pom.xml
@@ -42,6 +42,7 @@
org.projectlombok
lombok
true
+ provided
org.mapstruct
@@ -62,6 +63,10 @@
spring-boot-starter-test
test
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
@@ -145,6 +150,10 @@
org.apache.maven.plugins
maven-compiler-plugin
+
+ 21
+ 21
+
diff --git a/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/resttestclient/RestTestClientTest.java b/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/resttestclient/RestTestClientTest.java
new file mode 100644
index 000000000000..991e79d02401
--- /dev/null
+++ b/spring-boot-modules/spring-boot-4/src/test/java/com/baeldung/spring/resttestclient/RestTestClientTest.java
@@ -0,0 +1,168 @@
+package com.baeldung.spring.resttestclient;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.web.servlet.client.RestTestClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.WebApplicationContext;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@SpringBootTest
+public class RestTestClientTest {
+
+ @Autowired
+ private MyController myController;
+
+ @Autowired
+ private AnotherController anotherController;
+
+ @Autowired
+ private RestTestClient restTestClient;
+
+ @BeforeEach
+ void beforeEach(WebApplicationContext context) {
+ restTestClient = RestTestClient.bindToController(myController, anotherController)
+ .build();
+ }
+
+ @Test
+ void givenValidPath_WhenCalled_ThenReturnOk() {
+ restTestClient.get()
+ .uri("/persons/1")
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(Person.class)
+ .isEqualTo(new Person(1L, "John Doe"));
+ }
+
+ @Test
+ void givenWrongCallType_WhenCalled_ThenReturnClientError() {
+ restTestClient.post() // <=== wrong type
+ .uri("/persons/1")
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .is4xxClientError();
+ }
+
+ @Test
+ void givenWrongId_WhenCalled_ThenReturnNoContent() {
+ restTestClient.get()
+ .uri("/persons/0") // <=== wrong id
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .isNoContent();
+ }
+
+ @Test
+ void givenInvalidPath_WhenCalled_ThenReturnNotFound() {
+ restTestClient.get()
+ .uri("/invalid")
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .isNotFound();
+ }
+
+ @Test
+ void givenValidId_whenGetPerson_thenReturnsCorrectFields() {
+ restTestClient.get()
+ .uri("/persons/1")
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody()
+ .jsonPath("$.id")
+ .isEqualTo(1)
+ .jsonPath("$.name")
+ .isEqualTo("John Doe");
+ }
+
+ @Test
+ void givenValidRequest_whenGetPerson_thenPassesAllAssertions() {
+ restTestClient.get()
+ .uri("/persons/1")
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(Person.class)
+ .consumeWith(result -> {
+ assertThat(result.getStatus()
+ .value()).isEqualTo(200);
+ assertThat(result.getResponseBody()
+ .name()).isEqualTo("John Doe");
+ });
+ }
+
+ @Test
+ void givenValidQuery_whenGetPersonsStream_thenReturnsFlux() {
+ restTestClient.get()
+ .uri("/persons")
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(new ParameterizedTypeReference>() {});
+ }
+
+ @Test
+ void givenValidQueryToSecondController_whenGetPenguinMono_thenReturnsEmpty() {
+ restTestClient.get()
+ .uri("/pink/penguin")
+ .accept(MediaType.APPLICATION_JSON)
+ .exchange()
+ .expectStatus()
+ .isOk()
+ .expectBody(Penguin.class)
+ .value(it -> assertThat(it).isNull());
+ }
+}
+
+@RestController("my")
+class MyController {
+
+ @GetMapping("/persons/{id}")
+ public ResponseEntity getPersonById(@PathVariable Long id) {
+ return id == 1 ? ResponseEntity.ok(new Person(1L, "John Doe")) : ResponseEntity.noContent()
+ .build();
+ }
+
+ @GetMapping("/persons")
+ public Flux getAllPersons() {
+ var persons = List.of(
+ new Person(1L, "John Doe"),
+ new Person(2L, "James Bond"),
+ new Person(3L, "Alice In Wonderland")
+ );
+ return Flux.fromIterable(persons);
+ }
+}
+
+@RestController("my2")
+class AnotherController {
+
+ @GetMapping("/pink/penguin")
+ public Mono getPinkPenguin() {
+ return Mono.empty();
+ }
+}
+
+record Person(Long id, String name) { }
+record Penguin(Long id) { }