REST Assured with Spring Boot: Testing REST Endpoints
Spring Boot and REST Assured are a natural combination. Spring Boot makes building REST APIs fast; REST Assured makes testing them readable. The combination gives you a complete Java API testing stack that covers everything from controller unit tests to full integration tests against a running server.
This guide covers three levels of REST Assured + Spring Boot integration, from fastest (mock layer) to most realistic (actual HTTP).
The Three Testing Levels
Spring Boot tests exist on a spectrum:
- MockMvc-based — tests hit controller layer, Spring context loads, no actual HTTP
@SpringBootTestwith random port — real HTTP, real server, real port- External integration tests — test against deployed server (staging/prod)
REST Assured supports all three. The right choice depends on what you're testing and how fast feedback needs to be.
Dependencies
<dependencies>
<!-- Spring Boot Test (includes MockMvc) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- REST Assured + Spring MockMvc integration -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<!-- REST Assured core (for real HTTP tests) -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>Sample Application
Here's the Spring Boot REST API we'll test:
// src/main/java/com/example/UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<Page<UserDto>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String search
) {
Page<UserDto> users = userService.findUsers(page, size, search);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDto created = userService.createUser(request);
URI location = URI.create("/api/users/" + created.getId());
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request
) {
return userService.updateUser(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
if (!userService.deleteUser(id)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.noContent().build();
}
}Level 1: MockMvc-Based Tests
RestAssuredMockMvc wraps Spring's MockMvc with REST Assured's fluent API. Tests load the full Spring context but don't start an HTTP server — they're faster and work without a network:
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
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.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Optional;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.BDDMockito.*;
@WebMvcTest(UserController.class)
class UserControllerMockMvcTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@BeforeEach
void setup() {
RestAssuredMockMvc.mockMvc(mockMvc);
}
@Test
void shouldReturnUserById() {
UserDto user = new UserDto(1L, "Alice", "alice@example.com");
given(userService.findById(1L)).willReturn(Optional.of(user));
given()
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("name", equalTo("Alice"))
.body("email", equalTo("alice@example.com"));
}
@Test
void shouldReturn404ForMissingUser() {
given(userService.findById(999L)).willReturn(Optional.empty());
given()
.when()
.get("/api/users/999")
.then()
.statusCode(404);
}
@Test
void shouldCreateUser() {
String requestBody = """
{
"name": "Bob",
"email": "bob@example.com"
}
""";
UserDto created = new UserDto(42L, "Bob", "bob@example.com");
given(userService.createUser(any())).willReturn(created);
given()
.contentType("application/json")
.body(requestBody)
.when()
.post("/api/users")
.then()
.statusCode(201)
.header("Location", containsString("/api/users/42"))
.body("id", equalTo(42))
.body("name", equalTo("Bob"));
}
@Test
void shouldValidateCreateUserRequest() {
String invalidBody = """
{
"name": "",
"email": "not-an-email"
}
""";
given()
.contentType("application/json")
.body(invalidBody)
.when()
.post("/api/users")
.then()
.statusCode(400);
}
@Test
void shouldReturnUserList() {
List<UserDto> users = List.of(
new UserDto(1L, "Alice", "alice@example.com"),
new UserDto(2L, "Bob", "bob@example.com")
);
Page<UserDto> page = new PageImpl<>(users);
given(userService.findUsers(0, 20, null)).willReturn(page);
given()
.when()
.get("/api/users")
.then()
.statusCode(200)
.body("content.size()", equalTo(2))
.body("content[0].name", equalTo("Alice"))
.body("content[1].name", equalTo("Bob"));
}
}Level 2: Full Integration Tests
@SpringBootTest(webEnvironment = RANDOM_PORT) starts a real HTTP server on a random port. Tests make actual HTTP requests against your running application:
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class UserApiIntegrationTest {
@LocalServerPort
private int port;
@BeforeEach
void setup() {
RestAssured.port = port;
RestAssured.basePath = "/api";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
void shouldCreateAndRetrieveUser() {
// Create user
String requestBody = """
{
"name": "Charlie",
"email": "charlie@example.com"
}
""";
int userId = given()
.contentType("application/json")
.body(requestBody)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("Charlie"))
.extract()
.path("id");
// Retrieve the created user
given()
.when()
.get("/users/" + userId)
.then()
.statusCode(200)
.body("id", equalTo(userId))
.body("name", equalTo("Charlie"))
.body("email", equalTo("charlie@example.com"));
}
@Test
void shouldUpdateUser() {
// First create a user
int userId = createTestUser("Dave", "dave@example.com");
// Update the user
String updateBody = """
{
"name": "David",
"email": "david@example.com"
}
""";
given()
.contentType("application/json")
.body(updateBody)
.when()
.put("/users/" + userId)
.then()
.statusCode(200)
.body("name", equalTo("David"))
.body("email", equalTo("david@example.com"));
}
@Test
void shouldDeleteUser() {
int userId = createTestUser("Eve", "eve@example.com");
given()
.when()
.delete("/users/" + userId)
.then()
.statusCode(204);
// Verify deleted
given()
.when()
.get("/users/" + userId)
.then()
.statusCode(404);
}
@Test
void shouldSearchUsers() {
createTestUser("Frank", "frank@example.com");
createTestUser("Frances", "frances@example.com");
createTestUser("George", "george@example.com");
given()
.queryParam("search", "frank")
.when()
.get("/users")
.then()
.statusCode(200)
.body("content.size()", greaterThanOrEqualTo(1))
.body("content.name", everyItem(containsStringIgnoringCase("frank")));
}
private int createTestUser(String name, String email) {
return given()
.contentType("application/json")
.body(String.format("""
{"name": "%s", "email": "%s"}
""", name, email))
.when()
.post("/users")
.then()
.statusCode(201)
.extract()
.path("id");
}
}Test Profile Configuration
Create src/test/resources/application-test.yml to override settings for tests:
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
database-platform: org.hibernate.dialect.H2Dialect
# Disable external services in test
mail:
host: localhost
port: 25
logging:
level:
com.example: DEBUG
org.springframework.web: DEBUGAdd H2 dependency:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>Testing Security
If your Spring Boot app uses Spring Security, configure authentication in tests:
Basic Auth
given()
.auth().basic("admin", "password")
.when()
.get("/api/admin/users")
.then()
.statusCode(200);Bearer Token
String token = getAuthToken(); // Your token retrieval logic
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/api/protected")
.then()
.statusCode(200);Getting a Token from Your Auth Endpoint
@BeforeAll
static void authenticate() {
String authBody = """
{"username": "testuser", "password": "testpass"}
""";
token = given()
.contentType("application/json")
.body(authBody)
.when()
.post("/api/auth/login")
.then()
.statusCode(200)
.extract()
.path("token");
}MockMvc with Security
@WebMvcTest(UserController.class)
@WithMockUser(username = "testuser", roles = {"USER"})
class SecuredUserControllerTest {
@Test
void shouldAllowAuthenticatedUser() {
given()
.when()
.get("/api/users/1")
.then()
.statusCode(200);
}
}Testing Error Responses
@Test
void shouldReturn400ForInvalidInput() {
given()
.contentType("application/json")
.body("{\"email\": \"not-an-email\"}")
.when()
.post("/api/users")
.then()
.statusCode(400)
.body("status", equalTo(400))
.body("errors", hasSize(greaterThan(0)))
.body("errors[0].field", notNullValue())
.body("errors[0].message", notNullValue());
}
@Test
void shouldReturn409ForDuplicateEmail() {
String email = "duplicate@example.com";
// Create user first time — succeeds
given()
.contentType("application/json")
.body(String.format("{\"name\":\"User\",\"email\":\"%s\"}", email))
.when()
.post("/api/users")
.then()
.statusCode(201);
// Create user second time — conflict
given()
.contentType("application/json")
.body(String.format("{\"name\":\"User2\",\"email\":\"%s\"}", email))
.when()
.post("/api/users")
.then()
.statusCode(409)
.body("message", containsString("already exists"));
}Request/Response Specifications
Share common setup using RequestSpecification and ResponseSpecification:
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
class BaseIntegrationTest {
@LocalServerPort
protected int port;
protected RequestSpecification requestSpec;
protected ResponseSpecification successResponseSpec;
@BeforeEach
void setupSpecs() {
requestSpec = new RequestSpecBuilder()
.setPort(port)
.setBasePath("/api")
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.addHeader("Authorization", "Bearer " + getTestToken())
.build();
successResponseSpec = new ResponseSpecBuilder()
.expectStatusCode(200)
.expectHeader("Content-Type", containsString("application/json"))
.build();
}
}
class UserApiTest extends BaseIntegrationTest {
@Test
void shouldGetUser() {
given()
.spec(requestSpec)
.when()
.get("/users/1")
.then()
.spec(successResponseSpec)
.body("id", equalTo(1));
}
}Testing Pagination
@Test
void shouldPaginateUsers() {
// Create 25 users
for (int i = 0; i < 25; i++) {
createTestUser("User" + i, "user" + i + "@example.com");
}
// First page
given()
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/users")
.then()
.statusCode(200)
.body("content.size()", equalTo(10))
.body("totalElements", greaterThanOrEqualTo(25))
.body("totalPages", greaterThanOrEqualTo(3))
.body("first", equalTo(true))
.body("last", equalTo(false));
// Last page
given()
.queryParam("page", 2)
.queryParam("size", 10)
.when()
.get("/api/users")
.then()
.statusCode(200)
.body("content.size()", greaterThan(0))
.body("last", equalTo(true));
}Summary
REST Assured with Spring Boot gives you three powerful testing options:
| Level | Class | Speed | Realism |
|---|---|---|---|
| MockMvc | @WebMvcTest + RestAssuredMockMvc |
Fastest | Controller layer only |
| Integration | @SpringBootTest(RANDOM_PORT) |
Medium | Full stack, real HTTP |
| External | RestAssured.baseURI = "https://..." |
Slowest | Production-like |
Use MockMvc tests for controller-specific behavior (validation, routing, response shape). Use integration tests for business logic that spans layers. Use external tests to smoke-test deployed environments.
Combine with JUnit 5 for parameterized tests and test lifecycle management, and explore REST Assured's authentication options for testing secured endpoints comprehensively.