REST Assured with Spring Boot: Integration Testing Guide
Spring Boot's testing support is excellent on its own, but pairing it with REST Assured gives you expressive, readable integration tests that cover the full HTTP layer — controllers, serialization, validation, error handling, and everything in between. This guide shows you how to wire the two together and build a solid integration test suite.
Why Integration Tests Matter for Spring Boot APIs
Unit tests cover business logic in isolation. Integration tests verify that your Spring Boot application behaves correctly as a whole: that your controllers map routes correctly, that Jackson serializes your DTOs the way you expect, that your validation annotations reject bad input, and that your error handlers return the right status codes and error bodies.
REST Assured is well-suited for this because it lets you write these assertions in a readable, HTTP-native style that makes intent obvious even to reviewers who aren't deeply familiar with the codebase.
Project Setup
Add the REST Assured Spring Mock MVC dependency alongside the standard test starters:
<dependencies>
<!-- Spring Boot test starter (includes JUnit 5, Mockito, etc.) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- REST Assured core -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<!-- Spring Mock MVC integration -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>The spring-mock-mvc module lets REST Assured test through Spring's MockMvc instead of making real HTTP calls. This is faster than starting a full embedded server and works without a network port.
Two Approaches: MockMvc vs. Real Server
Approach 1: MockMvc (faster, no port)
Use this when you want fast tests that still cover the full Spring MVC stack:
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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static org.hamcrest.Matchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerMockMvcTest {
@Autowired
MockMvc mockMvc;
@BeforeEach
void setup() {
RestAssuredMockMvc.mockMvc(mockMvc);
}
@Test
void getUser_returnsCorrectFields() {
given()
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("email", notNullValue());
}
}Approach 2: Real embedded server (closer to production)
Use @SpringBootTest(webEnvironment = RANDOM_PORT) when you need real HTTP — useful for testing filters, security, and anything that doesn't work through MockMvc:
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 static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@LocalServerPort
int port;
@BeforeEach
void setup() {
RestAssured.port = port;
RestAssured.baseURI = "http://localhost";
}
@Test
void getUser_returnsCorrectFields() {
given()
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1));
}
}The @LocalServerPort annotation injects the randomly assigned port, so tests don't conflict with other running services.
Testing CRUD Endpoints
Here's a realistic test class covering a full resource lifecycle:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductApiTest {
@LocalServerPort int port;
@BeforeEach
void setup() {
RestAssured.port = port;
RestAssured.baseURI = "http://localhost";
RestAssured.basePath = "/api/v1";
}
@Test
void createProduct_returnsCreatedWithId() {
String body = """
{
"name": "Widget Pro",
"price": 29.99,
"stock": 100
}
""";
given()
.contentType("application/json")
.body(body)
.when()
.post("/products")
.then()
.statusCode(201)
.header("Location", containsString("/products/"))
.body("id", notNullValue())
.body("name", equalTo("Widget Pro"));
}
@Test
void getProduct_notFound_returns404() {
given()
.when()
.get("/products/999999")
.then()
.statusCode(404)
.body("error", equalTo("Product not found"))
.body("status", equalTo(404));
}
@Test
void updateProduct_returnsUpdatedData() {
// First create
int id = given()
.contentType("application/json")
.body("{\"name\":\"Old Name\",\"price\":10.00,\"stock\":5}")
.when()
.post("/products")
.then()
.statusCode(201)
.extract().path("id");
// Then update
given()
.contentType("application/json")
.body("{\"name\":\"New Name\",\"price\":15.00,\"stock\":5}")
.when()
.put("/products/" + id)
.then()
.statusCode(200)
.body("name", equalTo("New Name"))
.body("price", equalTo(15.00f));
}
@Test
void deleteProduct_returnsNoContent() {
int id = given()
.contentType("application/json")
.body("{\"name\":\"Temp\",\"price\":1.00,\"stock\":1}")
.when()
.post("/products")
.then()
.statusCode(201)
.extract().path("id");
given()
.when()
.delete("/products/" + id)
.then()
.statusCode(204);
// Verify it's gone
given()
.when()
.get("/products/" + id)
.then()
.statusCode(404);
}
}Testing Validation
Spring Boot's @Valid annotation triggers Bean Validation on incoming request bodies. Your integration tests should cover the rejection cases:
@Test
void createProduct_missingName_returns400() {
given()
.contentType("application/json")
.body("{\"price\":10.00,\"stock\":5}")
.when()
.post("/products")
.then()
.statusCode(400)
.body("errors", hasItem(containsString("name")));
}
@Test
void createProduct_negativePrice_returns400() {
given()
.contentType("application/json")
.body("{\"name\":\"Widget\",\"price\":-1.00,\"stock\":5}")
.when()
.post("/products")
.then()
.statusCode(400);
}Testing with TestContainers
If your API depends on a real database, use Testcontainers to spin up a containerized database for integration tests. REST Assured works the same way — the database is just running in a container:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class ProductApiWithDatabaseTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@LocalServerPort int port;
@BeforeEach
void setup() {
RestAssured.port = port;
RestAssured.baseURI = "http://localhost";
}
@Test
void productPersistsAfterCreation() {
int id = given()
.contentType("application/json")
.body("{\"name\":\"Durable Widget\",\"price\":9.99,\"stock\":10}")
.when()
.post("/api/products")
.then()
.statusCode(201)
.extract().path("id");
given()
.when()
.get("/api/products/" + id)
.then()
.statusCode(200)
.body("name", equalTo("Durable Widget"));
}
}Add the Testcontainers dependency:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>Testing Spring Security
When Spring Security is active, unauthenticated requests should return 401 and authorized requests should succeed. REST Assured handles this cleanly:
@Test
void protectedEndpoint_withoutToken_returns401() {
given()
.when()
.get("/api/admin/users")
.then()
.statusCode(401);
}
@Test
void protectedEndpoint_withValidToken_returns200() {
String token = obtainJwtToken("admin@example.com", "password");
given()
.header("Authorization", "Bearer " + token)
.when()
.get("/api/admin/users")
.then()
.statusCode(200);
}
private String obtainJwtToken(String email, String password) {
return given()
.contentType("application/json")
.body("{\"email\":\"" + email + "\",\"password\":\"" + password + "\"}")
.when()
.post("/api/auth/login")
.then()
.statusCode(200)
.extract()
.path("token");
}Test Data Management
Integration tests that write to a database need cleanup between tests. Two common approaches:
@Transactional on each test — rolls back after each test automatically. Works for simple cases but can hide transaction-boundary bugs.
@Sql to reset state — explicitly truncate tables before each test:
@Sql(scripts = "/test-data/truncate-products.sql",
executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Test
void listProducts_emptyDatabase_returnsEmptyArray() {
given()
.when()
.get("/api/products")
.then()
.statusCode(200)
.body("$", hasSize(0));
}Keeping Tests Fast
Integration tests are slower than unit tests by nature, but a few practices help:
- Use
@SpringBootTestwithRANDOM_PORTonly when you need real HTTP. Use MockMvc otherwise. - Share the Spring context between test classes with
@DirtiesContextsparingly — context startup is expensive. - Use a single shared Testcontainers instance across all tests with
@Container staticrather than spinning up a new container per test class. - Run integration tests in a separate Maven phase from unit tests using the Failsafe plugin.
Closing Thoughts
REST Assured and Spring Boot are a natural pair for Java API integration testing. The fluent DSL makes test intent readable, Spring Boot's test slice annotations give you control over what gets loaded, and Testcontainers closes the gap between test and production environments. Start with the embedded server approach, add Testcontainers when you need a real database, and keep your test data management explicit.
For teams where not everyone writes Java — QA engineers, product managers, junior testers — tools like HelpMeTest can complement this setup by letting non-developers run API scenarios in plain English without touching the Java test suite.