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:
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:
| Scope | Mechanism | Behavior Exercised |
|---|---|---|
| Plain unit | JUnit and ordinary constructors | Controller or service Java logic without Spring |
| MVC slice | @WebMvcTest and MockMvc | Routing, binding, validation, serialization, filters, and controller advice |
| Full context with mock web layer | @SpringBootTest and @AutoConfigureMockMvc | Application wiring plus simulated Servlet requests |
| Full live server | @SpringBootTest(webEnvironment = RANDOM_PORT) | Embedded server, filters, serialization, networking, and complete application wiring |
| Deployed-system test | External HTTP client | Packaged 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:
<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:
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:
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:
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:
@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:
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:
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:
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.
Comments
Read or comment on GitHub (sign-in required to comment)