REST Assured with Spring Boot: Testing REST Endpoints

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:

  1. MockMvc-based — tests hit controller layer, Spring context loads, no actual HTTP
  2. @SpringBootTest with random port — real HTTP, real server, real port
  3. 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: DEBUG

Add 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.

Read more