Testing Spring Boot REST APIs

Last update: 2026-08-17 10:18:23 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

Testing a Spring Boot REST API requires several scopes because a direct Java call, a simulated MVC request, and a request through a live server exercise different failure boundaries. A useful test suite places most behavior checks below the Spring context, then adds narrower web slices and a smaller number of full-application tests.

The dependency examples target Spring Boot 3.x because its test starter uses JUnit 5. Spring Boot 4 manages JUnit 6; Jupiter concepts remain, but package names for several Boot test annotations changed.

Testing without a Test Framework #

A REST API can be checked without JUnit, Spring Test, or Mockito. Start the application, send a request with the JDK HTTP client, and throw an error when the response violates the contract:

java
package com.example;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public final class ManualApiCheck {
    public static void main(String[] args) throws Exception {
        var request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080/greetings/Ada"))
                .header("Accept", "application/json")
                .GET()
                .build();

        var response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new AssertionError("status: " + response.statusCode());
        }
        if (!response.body().equals("{\"message\":\"Hello, Ada\"}")) {
            throw new AssertionError("body: " + response.body());
        }
    }
}

Java’s assert statement could replace the explicit checks, but assertions are disabled unless the JVM starts with -ea. Explicit AssertionError therefore gives a reliable process exit code.

A custom executable remains useful for smoke checks against an already deployed service. Replacing a test framework entirely would also require implementing test discovery, fixture lifecycle, isolation, result reporting, parallel execution, and build integration. Manual curl or browser checks provide exploration rather than a repeatable regression suite.

Choosing a Test Scope #

Spring’s testing documentation distinguishes unit tests from integration tests and provides several web-test scopes. No single annotation is the recommended answer for every endpoint. Select the smallest scope that contains the behavior under examination:

ScopeMechanismBehavior Exercised
Plain unitJUnit and ordinary constructorsController or service Java logic without Spring
MVC slice@WebMvcTest and MockMvcRouting, binding, validation, serialization, filters, and controller advice
Full context with mock web layer@SpringBootTest and @AutoConfigureMockMvcApplication wiring plus simulated Servlet requests
Full live server@SpringBootTest(webEnvironment = RANDOM_PORT)Embedded server, filters, serialization, networking, and complete application wiring
Deployed-system testExternal HTTP clientPackaged service and its deployed dependencies

Spring recommends unit tests where the Spring container is not required. MockMvc then provides full Spring MVC request handling without a running server. Keep some live-server tests because a simulated request does not cover the Servlet container’s network, dispatch, and error-page behavior.

Tests that use a database or broker need a separate dependency decision:

  • Replace the boundary with a fake or mock when testing application decisions.
  • Use an isolated real service when database queries, mappings, transactions, broker behavior, or wire compatibility form part of the requirement.
  • Never point an automated test at production infrastructure.

Spring Boot Test Dependencies #

An application that already inherits from the Spring Boot 3 parent or imports its BOM needs one test dependency:

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

The starter includes Spring Boot Test, Spring Test, JUnit Jupiter, Mockito, AssertJ, Hamcrest, JSONassert, JsonPath, and Awaitility. Let Spring Boot manage their versions because the dependency set is tested as one release train.

The examples use the following application boundary:

java
public interface GreetingService {
    Greeting greet(String name);
}

public record Greeting(String message) {}

@RestController
@RequestMapping("/greetings")
public final class GreetingController {
    private final GreetingService service;

    public GreetingController(GreetingService service) {
        this.service = service;
    }

    @GetMapping("/{name}")
    Greeting get(@PathVariable String name) {
        return service.greet(name);
    }
}

Plain JUnit 5 Tests #

Spring components remain ordinary Java objects. Constructing a controller directly avoids context startup when the test concerns branching, returned values, or service delegation:

java
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class GreetingControllerTest {
    @Test
    void returnsTheServiceResult() {
        GreetingService service =
                name -> new Greeting("Hello, " + name);
        var controller = new GreetingController(service);

        assertEquals(
                new Greeting("Hello, Ada"),
                controller.get("Ada"));
    }
}

The test does not validate @GetMapping, path-variable conversion, JSON serialization, validation, filters, or exception handlers because direct method invocation bypasses Spring MVC.

MVC Slice Tests with JUnit 5 #

@WebMvcTest loads MVC infrastructure and selected web components instead of the complete application. Supply a deterministic implementation for the controller dependency to keep Mockito out of the test:

java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
@Import(GreetingControllerWebTest.Dependencies.class)
class GreetingControllerWebTest {
    @Autowired
    MockMvc mvc;

    @Test
    void rendersAJsonGreeting() throws Exception {
        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(
                        "application/json"))
                .andExpect(jsonPath("$.message").value("Hello, Ada"));
    }

    @TestConfiguration(proxyBeanMethods = false)
    static class Dependencies {
        @Bean
        GreetingService greetingService() {
            return name -> new Greeting("Hello, " + name);
        }
    }
}

Add security configuration, controller advice, converters, or filters to the slice when their behavior belongs to the endpoint contract. A slice that excludes a production component under test can pass while the assembled application fails, so full-context coverage remains necessary.

Full-Context Tests #

The default @SpringBootTest web environment is MOCK. Add @AutoConfigureMockMvc to execute requests through the complete application context without opening a network port:

java
@SpringBootTest
@AutoConfigureMockMvc
class GreetingApplicationTest {
    @Autowired
    MockMvc mvc;

    @Test
    void servesAGreeting() throws Exception {
        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Hello, Ada"));
    }
}

Use a random-port environment when the embedded server or real HTTP behavior forms part of the test:

java
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GreetingHttpTest {
    @LocalServerPort
    int port;

    @Test
    void servesAGreetingOverHttp() throws Exception {
        var request = HttpRequest.newBuilder()
                .uri(URI.create(
                        "http://localhost:" + port + "/greetings/Ada"))
                .GET()
                .build();

        var response = HttpClient.newHttpClient().send(
                request, HttpResponse.BodyHandlers.ofString());

        assertEquals(200, response.statusCode());
        assertEquals(
                "{\"message\":\"Hello, Ada\"}",
                response.body());
    }
}

Random ports prevent fixed-port conflicts during concurrent builds. Client and server code run on separate threads, so a transaction started on the test thread does not automatically roll back data committed by a live-server request. Reset state explicitly or isolate the backing service.

Boot 3 also provides TestRestTemplate, while Spring’s reactive stack provides WebTestClient. Spring Framework 7 adds RestTestClient for a common synchronous assertion API across MockMvc and live-server tests. Choose a client supported by the Spring version in the application rather than copying an unversioned example.

JUnit 5 and Mockito without a Spring Context #

Use MockitoExtension when Spring wiring is not under test:

java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class GreetingControllerMockitoTest {
    @Mock
    GreetingService service;

    @InjectMocks
    GreetingController controller;

    @Test
    void delegatesToTheService() {
        when(service.greet("Ada"))
                .thenReturn(new Greeting("Welcome, Ada"));

        assertEquals(
                new Greeting("Welcome, Ada"),
                controller.get("Ada"));
        verify(service).greet("Ada");
    }
}

The test loads no Spring context. @InjectMocks calls the controller constructor, while @Mock creates an ordinary Mockito object.

Mockito in a Spring Test Context #

An ordinary @Mock field does not replace a bean in Spring’s ApplicationContext. Use @MockitoBean when a slice or full-context test needs a bean override:

java
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
class GreetingControllerMockitoWebTest {
    @Autowired
    MockMvc mvc;

    @MockitoBean
    GreetingService service;

    @Test
    void rendersTheMockedServiceResult() throws Exception {
        when(service.greet("Ada"))
                .thenReturn(new Greeting("Welcome, Ada"));

        mvc.perform(get("/greetings/Ada"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.message").value("Welcome, Ada"));

        verify(service).greet("Ada");
    }
}

Spring Framework’s TestContext support creates the mock and replaces the matching bean before refreshing the context. Spring Boot test annotations already register SpringExtension, so adding MockitoExtension does not help with context bean replacement.

@MockitoSpyBean wraps an existing context bean when selected real behavior must remain. Spring Boot’s older @MockBean and @SpyBean annotations were deprecated in Boot 3.4 in favor of Spring Framework’s @MockitoBean and @MockitoSpyBean. Projects on Boot 3.3 or earlier must use the older annotations.

Apply bean overrides to architectural boundaries such as remote clients, message publishers, clocks, or repositories. Replacing many internal beans in a full-context test removes the wiring that @SpringBootTest was selected to exercise; a plain unit test or MVC slice then expresses the scope more accurately.

References #

Comments

Read or comment on GitHub (sign-in required to comment)