Spring Boot @WebFluxTest and WebTestClient Example

Test a Spring WebFlux controller with @WebFluxTest, WebTestClient and @MockitoBean on Spring Boot 4.1.2. Verify JSON, 404 responses and the difference from a live-server test.

Spring boot webflux test example

Use @WebFluxTest to test a Spring WebFlux controller’s HTTP behavior without starting an HTTP server. The annotation loads the selected controller and WebFlux infrastructure, and Spring Boot supplies a WebTestClient that sends requests through that test context. A mock service gives the controller predictable responses while the test checks the status code and JSON body.

This example uses Spring Boot 4.1.2 and @MockitoBean, the Spring Framework annotation that supplies a mock bean to the test context. The tests check an order that exists and an order that does not.

1. Set Up a WebFlux Controller Test

The application needs the WebFlux starter. The test starter adds Spring Boot’s WebFlux test support, JUnit, Mockito and WebTestClient. With Spring Boot’s dependency management, neither starter needs its own version in the project pom.xml.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux-test</artifactId>
  <scope>test</scope>
</dependency>

The following distinction determines what a passing test tells us. A slice test exercises HTTP mapping, WebFlux request handling, response serialization and the selected controller. It does not run the real service when that service is mocked.

Test setup Application context HTTP server
@WebFluxTest(OrderStatusController.class) Selected controller and WebFlux test infrastructure No
@SpringBootTest(webEnvironment = RANDOM_PORT) Full application Yes

The Spring Boot testing reference describes which components the WebFlux slice includes. Start with a slice when the question is whether a controller turns a service result into the correct HTTP response.

2. Define the Order Status Endpoint

An order application lets a customer request the current status of an order. The controller receives an order ID from /orders/{orderId}/status and asks OrderStatusService for an OrderStatus. The service returns a Mono, a reactive publisher that emits either one status or no status.

public record OrderStatus(String orderId, String state) {}

public interface OrderStatusService {
  Mono<OrderStatus> findStatus(String orderId);
}

The controller maps a returned status to HTTP 200 with JSON. When the publisher is empty, defaultIfEmpty() supplies HTTP 404. The response type is Mono<ResponseEntity>, so both outcomes remain part of the same reactive chain.

@RestController
public class OrderStatusController {
  private final OrderStatusService service;

  public OrderStatusController(OrderStatusService service) {
    this.service = service;
  }

  @GetMapping("/orders/{orderId}/status")
  public Mono<ResponseEntity<OrderStatus>> status(@PathVariable String orderId) {
    return service.findStatus(orderId)
        .map(ResponseEntity::ok)
        .defaultIfEmpty(ResponseEntity.notFound().build());
  }
}

The complete project has an InMemoryOrderStatusService for an ordinary application run. The controller tests intentionally replace that collaborator with a mock, so each test controls the service result without depending on the sample service’s data.

3. Test an Existing Order

@WebFluxTest(OrderStatusController.class) creates the controller and WebFlux test infrastructure. The slice leaves out the regular @Service bean. @MockitoBean supplies an OrderStatusService mock so Spring can construct the controller. Boot injects the configured WebTestClient into the test.

The test makes the mock return a status for order A-17, sends a GET request and checks the JSON fields. The final verify() call checks that the controller passed the requested ID to its service.

@WebFluxTest(OrderStatusController.class)
class OrderStatusControllerTest {
  @Autowired WebTestClient client;
  @MockitoBean OrderStatusService service;

  @Test
  void returnsOrderStatusAsJson() {
    when(service.findStatus("A-17"))
        .thenReturn(Mono.just(new OrderStatus("A-17", "SHIPPED")));

    client.get().uri("/orders/A-17/status").exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$.orderId").isEqualTo("A-17")
        .jsonPath("$.state").isEqualTo("SHIPPED");

    verify(service).findStatus("A-17");
  }
}

exchange() submits the request to the mock WebFlux server bound to the test context. The response is HTTP 200 and contains {"orderId":"A-17","state":"SHIPPED"}. The assertion also catches a controller that ignores the path variable or returns the wrong field names. It does not prove that the real service can find that order, because the mock supplies the status.

4. Test an Order That Does Not Exist

The missing-order case changes one input and one service result. This time the mock returns Mono.empty(). No OrderStatus reaches map(), so defaultIfEmpty() supplies a 404 response with no body.

@Test
void returnsNotFoundWhenServiceIsEmpty() {
  when(service.findStatus("missing")).thenReturn(Mono.empty());

  client.get().uri("/orders/missing/status").exchange()
      .expectStatus().isNotFound()
      .expectBody().isEmpty();

  verify(service).findStatus("missing");
}

This assertion checks the controller’s missing-result rule rather than treating every service response as HTTP 200. If the controller returned an empty Mono<ResponseEntity> instead of constructing ResponseEntity.notFound(), this status assertion would expose the difference.

5. Know When to Use a Full Application Test

A passing @WebFluxTest proves the selected controller’s behavior in a test slice; it does not prove the real service, repository or running server works. Use a separate full-context test when those interactions are the subject of the test. With RANDOM_PORT, @SpringBootTest starts a server. In Spring Boot 4, add @AutoConfigureWebTestClient to make the client available in this full-context test.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class OrderStatusHttpTest {
  @Autowired WebTestClient client;

  @Test
  void readsStatusThroughRunningServer() {
    client.get().uri("/orders/A-17/status").exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$.state").isEqualTo("SHIPPED");
  }
}

This broader test uses the sample InMemoryOrderStatusService, so it checks the real service-to-controller path as well as the HTTP response. It remains an in-memory example, not proof of database or external API integration. See the Spring Framework WebTestClient reference for mock binding and live-server options.

6. Frequently Asked Questions

6.1. Does @WebFluxTest Start Netty?

No. It binds WebTestClient to WebFlux test infrastructure without starting a listening HTTP server. Use a full test with a running web environment when you need to test the actual network boundary.

6.2. Why Is @MockitoBean Needed If the Application Has a Service?

The regular service is outside the selected WebFlux slice, but the controller still needs an OrderStatusService constructor argument. @MockitoBean adds a mock bean to the slice. The test can then stub the exact publisher the controller should receive.

6.3. Can WebTestClient Test Functional Routes?

Yes. WebTestClient can test functional endpoints as well as annotated controllers. A functional route needs a suitable test setup, such as binding to a router function or loading the relevant application context. The selected-controller form of @WebFluxTest shown here is for the annotated OrderStatusController.

7. Conclusion

Use @WebFluxTest for fast tests of a WebFlux controller’s route, HTTP status and response body. Stub the service publisher with @MockitoBean, then test both a value and an empty result. Add a full application test when the real service or running server is the behavior you need to verify.

Happy Learning!

Complete WebFlux controller test example on GitHub

Leave a Comment

  1. I keep getting csrf token error.

    (An expected CSRF token cannot be found
    java.lang.AssertionError: Status expected:<201 CREATED> but was:<403 FORBIDDEN>
    Expected :201 CREATED
    Actual  :403 FORBIDDEN)

    But as per my security config I have disabled csrf as shown below:

    @Configuration
    @EnableWebFluxSecurity
    public class SecurityConfig {
        @Autowired
        private UserRepository userRepository;
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http){
            return http
                    .csrf(ServerHttpSecurity.CsrfSpec::disable)
                    .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec
                            .anyExchange()
                            .permitAll())
                    .build();
        }
    

    Would you mind giving me suggestions?

  2. how to do the same testing if WebFlux RouterFunction is used instead of RestController. How to tell to the test that it is WebFlux test?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.