Skip to content

Instantly share code, notes, and snippets.

@malteo
Created October 31, 2025 06:30
Show Gist options
  • Select an option

  • Save malteo/f34a811c6ac83cc89c74322073255be7 to your computer and use it in GitHub Desktop.

Select an option

Save malteo/f34a811c6ac83cc89c74322073255be7 to your computer and use it in GitHub Desktop.

Testing Spring REST APIs with Spring Boot 3

Learn how to test your Spring REST APIs using Spring Boot 3 and Spring Framework 6 testing tools. This guide walks you through five different testing approaches, from simple unit tests to full end-to-end tests.

Testing Tools in Spring Boot 3 / Spring Framework 6

Spring Boot 3 provides several mature testing tools for REST APIs:

  • MockMvc - Test Spring MVC controllers without a running server
  • WebTestClient - Reactive/functional style API for testing (works with both reactive and servlet stacks)
  • TestRestTemplate - Full HTTP testing with an embedded server

Why use these tools?

  • Battle-tested and widely adopted
  • Rich feature set for all types of tests
  • Excellent documentation and community support
  • Works for unit tests, integration tests, and end-to-end tests

Which Testing Approach Should I Use?

Spring Boot 3 offers 5 different ways to test your API. Here's a simple guide:

Method Speed What It Tests Best For
MockMvc (standalone) ⚑ Fastest Just your controller Quick unit tests
MockMvc (@WebMvcTest) πŸš€ Fast Controller + Spring MVC Testing validation, security
WebTestClient (@SpringBootTest) 🐒 Slower Full app (no HTTP) Real database tests, reactive apps
TestRestTemplate 🐌 Slowest Everything with HTTP Complete end-to-end tests
WebTestClient (RouterFunction) ⚑ Fastest Functional endpoints WebFlux functional routes

Not sure? Start with MockMvc standalone for simple tests and @SpringBootTest with WebTestClient for integration tests.

1. Unit Tests with MockMvc (Standalone)

What it does: Tests just your controller logic, nothing else. No Spring, no database, no validation.

When to use: When you want super-fast tests for your controller logic.

public class TodoControllerUnitTest {
    MockMvc mockMvc;
    TodoService todoService;

    @BeforeEach
    void setup() {
        // Mock your dependencies
        todoService = Mockito.mock(TodoService.class);
        when(todoService.getAllTodos()).thenReturn(
            List.of(new Todo(1L, 1L, "Test Todo", false))
        );

        // Create standalone MockMvc
        mockMvc = MockMvcBuilders
                .standaloneSetup(new TodoController(todoService))
                .build();
    }

    @Test
    void shouldGetAllTodos() throws Exception {
        mockMvc.perform(get("/api/todos"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title").value("Test Todo"));
    }
}

βœ… Pros: Super fast, simple setup ❌ Cons: No validation, security, or other Spring features


2. MVC Tests with @WebMvcTest

What it does: Tests your controller with Spring MVC features like validation, security, and exception handling.

When to use: When you need to test validation rules, security, or @ControllerAdvice error handling.

@WebMvcTest(TodoController.class)
public class TodoControllerMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private TodoService todoService;

    @Test
    void shouldValidateInput() throws Exception {
        // Test validation - empty title should fail
        mockMvc.perform(post("/api/todos")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"userId": null, "title": "", "completed": false}
                    """))
            .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser
    void shouldAllowAuthenticatedUser() throws Exception {
        when(todoService.getAllTodos()).thenReturn(
            List.of(new Todo(1L, 1L, "Secured Todo", false))
        );

        mockMvc.perform(get("/api/todos"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title").value("Secured Todo"));
    }
}

βœ… Pros: Tests validation, security, and error handling ❌ Cons: Services are still mocked, no real database


3. Integration Tests with @SpringBootTest and WebTestClient

What it does: Tests your full application with real services and database - but without actual HTTP.

When to use: When you want to test the complete flow including database operations.

@SpringBootTest
@AutoConfigureWebTestClient
@Transactional  // Rolls back after each test
public class TodoControllerIntegrationTest {

    @Autowired
    private WebTestClient webTestClient;

    @Autowired
    private TodoRepository todoRepository;

    @Autowired
    private TodoService todoService;

    @Test
    void shouldCreateAndRetrieveTodo() {
        // Create a todo - this hits the real database
        webTestClient.post()
                .uri("/api/todos")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(new Todo(null, 1L, "Integration Test", false))
                .exchange()
                .expectStatus().isCreated();

        // Verify it's in the database
        assertEquals(1, todoService.getAllTodos().size());

        // Retrieve it
        webTestClient.get()
                .uri("/api/todos")
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$[0].title").isEqualTo("Integration Test");
    }
}

Alternative: Using MockMvc instead of WebTestClient

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
public class TodoControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldCreateAndRetrieveTodo() throws Exception {
        mockMvc.perform(post("/api/todos")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"userId": 1, "title": "Integration Test", "completed": false}
                    """))
            .andExpect(status().isCreated());

        mockMvc.perform(get("/api/todos"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title").value("Integration Test"));
    }
}

βœ… Pros: Real database, real services, tests actual behavior ❌ Cons: Slower, can't test HTTP-specific features (like CORS)


4. End-to-End Tests with TestRestTemplate

What it does: Tests everything including real HTTP requests - the most complete testing possible.

When to use: When you need to test HTTP-specific features like CORS, headers, or compression.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerServerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void findAllTodos() {
        ResponseEntity<List<Todo>> response = restTemplate.exchange(
                "/api/todos",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Todo>>() {}
        );

        assertEquals(HttpStatus.OK, response.getStatusCode());
        List<Todo> todos = response.getBody();
        assertEquals(200, todos.size());
        assertEquals("delectus aut autem", todos.get(0).title());
        assertFalse(todos.get(0).completed());
    }

    @Test
    void createTodo() {
        Todo newTodo = new Todo(null, 1L, "New Todo", false);

        ResponseEntity<Todo> response = restTemplate.postForEntity(
                "/api/todos",
                newTodo,
                Todo.class
        );

        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertNotNull(response.getBody().id());
    }
}

Alternative: Using WebTestClient for end-to-end tests

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TodoControllerServerTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void findAllTodos() {
        webTestClient.get()
                .uri("/api/todos")
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(Todo.class)
                .hasSize(200)
                .value(todos -> {
                    assertEquals("delectus aut autem", todos.get(0).title());
                    assertFalse(todos.get(0).completed());
                });
    }
}

βœ… Pros: Most realistic, tests everything including HTTP ❌ Cons: Slowest, can be flaky due to network issues


5. Functional Tests with WebTestClient and RouterFunction

What it does: Tests functional/reactive endpoints (WebFlux style).

When to use: Only if you're using RouterFunction instead of @RestController.

public class TodoRouterTest {

    private TodoHandler todoHandler;
    private WebTestClient webTestClient;

    @BeforeEach
    void setup() {
        todoHandler = new TodoHandler(todoService);
        
        RouterFunction<ServerResponse> routes = RouterFunctions.route()
            .GET("/api/todos", todoHandler::getAllTodos)
            .POST("/api/todos", todoHandler::createTodo)
            .build();

        webTestClient = WebTestClient
                .bindToRouterFunction(routes)
                .build();
    }

    @Test
    void shouldTestFunctionalEndpoint() {
        webTestClient.get()
                .uri("/api/todos")
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(Todo.class);
    }
}

βœ… Pros: Fast, good for reactive apps ❌ Cons: Only works with RouterFunction (not @RestController)


Getting Started

Required Dependencies

Add these to your pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Optional: For WebTestClient support with servlet stack -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

For Gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    // Optional: For WebTestClient support
    testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
}

Running the Tests

# Run all tests
./mvnw test

# Run a specific test
./mvnw test -Dtest=TodoControllerMvcTest

Quick Tips

Test Pyramid - How Many of Each Test?

  • Many unit tests (standalone MockMvc) - fast feedback
  • Some integration tests (@SpringBootTest) - test real behavior
  • Few end-to-end tests (TestRestTemplate) - catch edge cases

Common Mistakes

1. Using @MockBean without Spring

// ❌ Won't work
public class Test {
    @MockBean TodoService service;
}

// βœ… Works
@WebMvcTest
public class Test {
    @MockBean TodoService service;
}

2. Testing HTTP features without a server

// ❌ CORS won't work here
@WebMvcTest

// βœ… Use this for CORS/headers
@SpringBootTest(webEnvironment = RANDOM_PORT)

3. Forgetting to add WebFlux dependency for WebTestClient

<!-- Need this in test scope to use WebTestClient with servlet stack -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <scope>test</scope>
</dependency>

Comparison: MockMvc vs WebTestClient vs TestRestTemplate

Feature MockMvc WebTestClient TestRestTemplate
Style Traditional Fluent/Reactive Traditional
HTTP Server No No (usually) Yes
Performance Fast Fast Slower
Reactive Support No Yes No
Best Use Case MVC testing Modern/reactive apps E2E testing

Migrating to Spring Framework 7's RestTestClient

When you're ready to upgrade to Spring Framework 7 (Spring Boot 4), the migration is straightforward:

From MockMvc

// Spring Boot 3
mockMvc.perform(get("/api/todos"))
    .andExpect(status().isOk());

// Spring Boot 4 / Framework 7
RestTestClient.bindToMockMvc(mockMvc)
    .get().uri("/api/todos")
    .exchange()
    .expectStatus().isOk();

From WebTestClient

// Spring Boot 3
webTestClient.get()
    .uri("/api/todos")
    .exchange()
    .expectStatus().isOk();

// Spring Boot 4 / Framework 7 - Very similar!
RestTestClient.bindToApplicationContext(context)
    .get().uri("/api/todos")
    .exchange()
    .expectStatus().isOk();

From TestRestTemplate

// Spring Boot 3
testRestTemplate.getForEntity("/api/todos", Todo[].class);

// Spring Boot 4 / Framework 7
RestTestClient.bindToServer()
    .get().uri("/api/todos")
    .exchange()
    .expectStatus().isOk();

Additional Resources


Adapted for Spring Boot 3 / Spring Framework 6 Original guide by Dan Vega

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment