Home Java Master REST API Testing with REST-assured in Spring Boot
Intermediate 5 min · July 14, 2026

Master REST API Testing with REST-assured in Spring Boot

Learn REST-assured testing in Spring Boot with production-tested patterns, debugging tips, and real incident stories from senior Java developers..

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of Spring Boot and REST APIs
  • Java 17+ and Maven/Gradle
  • Familiarity with JUnit 5
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Use REST-assured for fluent, readable REST API tests in Spring Boot.
  • Leverage @SpringBootTest with webEnvironment and @LocalServerPort for integration tests.
  • Validate JSON responses with JsonPath for precise assertions.
  • Mock external services with WireMock to isolate API tests.
  • Integrate with Testcontainers for database or message broker dependencies.
✦ Definition~90s read
What is REST API Testing with REST-assured?

REST-assured is a Java DSL for testing and validating RESTful web services, enabling you to write readable, maintainable API tests.

Think of REST-assured as a postman inside your code.
Plain-English First

Think of REST-assured as a postman inside your code. Instead of clicking buttons, you write Java code that sends HTTP requests and checks the responses automatically. It's like having a robot that can test your API endpoints every time you make a change.

If you're building REST APIs with Spring Boot, you need to test them. Not just unit tests that mock everything, but real integration tests that hit actual endpoints. That's where REST-assured comes in. It's a Java DSL that makes HTTP testing feel like natural language. You write something like given().when().get("/api/orders").then().statusCode(200) and it just works.

I've seen teams spend weeks debugging API contracts that broke because they only tested with Postman manually. The moment you automate with REST-assured, you catch regressions fast. But there's a catch: REST-assured has its own pitfalls. Port conflicts, SSL issues, and flaky tests due to async processing are common.

In this article, I'll show you how to set up REST-assured in a Spring Boot project the right way. We'll cover configuration, request/response validation, authentication, and advanced patterns like mocking external services. I'll also share war stories from production where REST-assured tests saved us (or failed us). By the end, you'll be able to write robust API tests that you can trust.

Setting Up REST-assured in Spring Boot

Let's get REST-assured into your project. I prefer using the latest version, currently 5.4.0, but check Maven Central. Add the dependency to your pom.xml:

``xml <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> ``

If you're using Spring Boot's dependency management, you don't need a version. But watch out: Spring Boot might bring an older version. I always override to the latest stable. Also add the JSON path support:

``xml <dependency> <groupId>io.rest-assured</groupId> <artifactId>json-path</artifactId> <scope>test</scope> </dependency> ``

Now, create a test class. The key is to use @SpringBootTest with a random port. Why random? Because fixed ports cause conflicts in CI. Here's a typical setup:

```java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class OrderControllerTest {

@LocalServerPort private int port;

@BeforeEach void setUp() { RestAssured.port = port; RestAssured.baseURI = "http://localhost"; } // tests... } ```

I've seen people forget to set the port and then wonder why tests fail. The @LocalServerPort annotation injects the actual random port. Don't hardcode it.

One more thing: if you have multiple test classes, consider using @DirtiesContext to avoid context caching issues. This is especially important when you have stateful beans.

OrderControllerTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
class OrderControllerTest {

    @LocalServerPort
    private int port;

    @BeforeEach
    void setUp() {
        RestAssured.port = port;
        RestAssured.baseURI = "http://localhost";
    }

    @Test
    void shouldCreateOrder() {
        String orderJson = """
                {
                    "customerId": 1,
                    "items": [
                        {"productId": 101, "quantity": 2}
                    ]
                }
                """;

        given()
            .contentType(ContentType.JSON)
            .body(orderJson)
        .when()
            .post("/api/orders")
        .then()
            .statusCode(201)
            .body("orderId", notNullValue());
    }
}
Output
Test passes if the API returns 201 and a non-null orderId.
💡Use static imports for readability
📊 Production Insight
In a microservices environment, random ports prevent conflicts when multiple test suites run in parallel on the same CI node.
🎯 Key Takeaway
Always use random ports and inject them via @LocalServerPort.

Writing Your First REST-assured Test

Let's write a simple test for a GET endpoint. Suppose we have an endpoint that returns a list of orders. I'll show you the test, then break it down.

``java @Test void shouldReturnOrders() { given() .param("page", 0) .param("size", 10) .when() .get("/api/orders") .then() .statusCode(200) .body("content.size()", greaterThan(0)) .body("content[0].customerId", equalTo(1)); } ``

Notice the static methods: given(), when(), then(). This is the BDD style. given() sets up request parameters, headers, body. when() executes the HTTP method. then() asserts on the response.

For query parameters, use .param(). For path parameters, use .pathParam(). For headers, .header(). For authentication, .auth().

Now, what if you need to extract a value from the response for subsequent tests? Use .extract():

``java int orderId = given() .contentType(JSON) .body(orderJson) .when() .post("/api/orders") .then() .statusCode(201) .extract() .path("orderId"); ``

This is powerful for chaining requests. But be careful: extracted values can be null if the path is wrong. Always validate the response structure first.

One mistake I often see: using then().log().all() for debugging but leaving it in the final code. Remove logging assertions in production tests to keep logs clean.

OrderApiTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@Test
void shouldReturnOrdersPaginated() {
    given()
        .param("page", 0)
        .param("size", 10)
    .when()
        .get("/api/orders")
    .then()
        .statusCode(200)
        .body("content.size()", greaterThan(0))
        .body("content[0].customerId", equalTo(1))
        .body("totalElements", greaterThan(0));
}
Output
Test passes if the response has at least one order in content and totalElements > 0.
🔥BDD vs. non-BDD styles
📊 Production Insight
In a SaaS billing system, we used extract() to get the newly created invoice ID and then immediately fetch it to verify persistence. This caught a race condition where the invoice was created but not yet indexed.
🎯 Key Takeaway
Use extract() to capture response data for chained tests, but validate the response first.

Validating JSON Responses with JsonPath

REST-assured uses JsonPath under the hood. You can write complex assertions like:

``java .body("orders.find { it.status == 'PENDING' }.total", greaterThan(100.0)) ``

This filters the orders array for pending orders and checks the total. JsonPath syntax is powerful but can be tricky. I recommend testing your JsonPath expressions in isolation first using a tool like JsonPath Tester.

Common gotcha: when using body() with Hamcrest matchers, the matcher must match the entire value. For partial matches, use containsString() or hasItem().

For arrays, you can assert size:

``java .body("orders.size()", equalTo(5)) ``

``java .body("customer.name", equalTo("John")) ``

But what if the key contains special characters? Escape them with brackets:

``java .body("['customer-name']", equalTo("John")) ``

One more thing: if you need to validate the entire response structure, consider using JSON Schema validation. REST-assured supports it via body(matchesJsonSchemaInClasspath("order-schema.json")). This is great for contract testing.

JsonPathAssertionTest.javaJAVA
1
2
3
4
5
6
given()
    .get("/api/orders/1")
.then()
    .body("customer.name", equalTo("Alice"))
    .body("items.size()", equalTo(3))
    .body("items.find { it.productId == 101 }.quantity", equalTo(2));
Output
Test passes if the order contains customer name 'Alice', 3 items, and product 101 has quantity 2.
⚠ JsonPath parsing errors
📊 Production Insight
We had a test that failed randomly because the order of items in the response changed. We switched to using containsInAnyOrder() for array assertions.
🎯 Key Takeaway
JsonPath allows powerful nested assertions, but test your expressions separately to avoid brittle tests.

Handling Authentication and Headers

Most APIs require authentication. REST-assured supports various auth schemes out of the box:

  • Basic Auth: given().auth().preemptive().basic("user", "pass")
  • OAuth2: given().auth().oauth2("token")
  • Custom header: given().header("Authorization", "Bearer " + token)

I prefer using preemptive basic auth for tests because it avoids an extra challenge request. But in production, you'd use OAuth2.

``java given().filter(new SessionFilter()).get("/api/orders") ``

But that's less common.

Now, a real-world scenario: you have an API that requires a CSRF token. You need to first fetch the token, then include it in subsequent requests. Here's how I do it:

```java // First, get CSRF token String csrfToken = given() .get("/api/csrf") .then() .extract() .path("token");

// Then, use it in POST given() .header("X-CSRF-TOKEN", csrfToken) .contentType(JSON) .body(orderJson) .when() .post("/api/orders") .then() .statusCode(201); ```

This pattern is common in Spring Security setups. But be careful: if the CSRF token changes per session, you need to reuse the same session. Use SessionFilter to maintain cookies.

AuthTest.javaJAVA
1
2
3
4
5
6
7
8
given()
    .auth().preemptive().basic("admin", "password")
    .contentType(JSON)
    .body(orderJson)
.when()
    .post("/api/orders")
.then()
    .statusCode(201);
Output
Test passes if the API accepts basic auth and returns 201.
💡Avoid hardcoding credentials
📊 Production Insight
In a fintech app, we had to test token refresh flows. We used REST-assured to simulate the entire OAuth2 dance, which caught a bug where the refresh token was not being rotated.
🎯 Key Takeaway
Use preemptive basic auth for simplicity, but prefer OAuth2 in production-like tests.

What the Official Docs Won't Tell You

Here's the hard truth: REST-assured documentation is good but leaves out several production gotchas.

  1. Static port configuration is global. If you set RestAssured.port in one test, it affects all tests running in the same JVM. Always reset it in @BeforeEach or use a fresh RequestSpecification per test class.
  2. @DirtiesContext is your friend. Without it, Spring caches the application context across test classes. If one test class modifies the database, other tests see dirty data. Use @DirtiesContext(classMode = AFTER_CLASS) to force a fresh context.
  3. REST-assured doesn't automatically follow redirects. If your API returns 302, you need to disable redirects or handle it manually. Use given().redirects().follow(false).
  4. Multipart uploads are tricky. The API for file uploads is not intuitive. You need to use given().multiPart("file", new File("test.txt")). But if you forget to set the content type, it defaults to application/octet-stream, which may not be what your server expects.
  5. SSL in tests. If you're testing HTTPS locally with a self-signed certificate, use given().relaxedHTTPSValidation(). But never use this in production tests.
  6. Parallel test execution. REST-assured is not thread-safe by default. If you run tests in parallel, each thread must have its own RestAssured instance or you'll get port conflicts. Use @TestInstance(Lifecycle.PER_CLASS) and reset state in @BeforeEach.
  7. Logging can mask failures. I've seen teams add log().all() to every test and then ignore the output. Only log when a test fails. Use given().log().ifValidationFails() to automatically log on failure.
AdvancedConfigTest.javaJAVA
1
2
3
4
5
6
7
8
9
given()
    .log().ifValidationFails()
    .redirects().follow(false)
    .relaxedHTTPSValidation()
    .multiPart("file", new File("test.txt"), "text/plain")
.when()
    .post("/api/upload")
.then()
    .statusCode(200);
Output
Test passes if the upload endpoint returns 200.
⚠ Don't forget to reset global state
📊 Production Insight
We once had a test suite that took 10 minutes to run. The culprit was a @DirtiesContext on every test method. We moved it to class level and reduced execution time by 80%.
🎯 Key Takeaway
Be aware of REST-assured's global state and Spring context caching to avoid flaky tests.

Mocking External Services with WireMock

Your API probably calls external services. In tests, you don't want to hit real endpoints. WireMock is the perfect companion. It runs a mock HTTP server that you can program to return specific responses.

``xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-contract-stub-runner</artifactId> <scope>test</scope> </dependency> ``

```java @SpringBootTest(webEnvironment = RANDOM_PORT) @WireMockTest(httpPort = 8089) class PaymentServiceTest {

@Test void shouldProcessPayment() { stubFor(post(urlEqualTo("/payments")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody("{\"status\":\"SUCCESS\"}")));

given() .contentType(JSON) .body(paymentRequest) .when() .post("/api/payments") .then() .statusCode(200) .body("status", equalTo("SUCCESS")); } } ```

You need to configure your Spring Boot app to use the WireMock port for external calls. I usually set the external service URL in application-test.properties:

``properties payment.service.url=http://localhost:8089 ``

And activate the test profile in your test class.

WireMock also supports request matching, delays, and fault injection. Use withFixedDelay(2000) to simulate slow responses.

WireMockTest.javaJAVA
1
2
3
4
5
stubFor(post(urlEqualTo("/payments"))
    .willReturn(aResponse()
        .withStatus(200)
        .withHeader("Content-Type", "application/json")
        .withBody("{\"status\":\"SUCCESS\"}")));
Output
WireMock returns a 200 response with JSON body for POST /payments.
🔥WireMock vs. MockServer
📊 Production Insight
In a microservices architecture, we used WireMock to simulate downstream failures. This helped us verify our circuit breaker logic without actually breaking the production dependency.
🎯 Key Takeaway
Use WireMock to simulate external services and make your tests deterministic.

Integrating with Testcontainers for Database Tests

If your API interacts with a database, you need a real database in your tests. Testcontainers spins up a Docker container with your database. This is much better than using an in-memory H2, which behaves differently from production databases.

``xml <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> ``

```java @Testcontainers @SpringBootTest(webEnvironment = RANDOM_PORT) class OrderRepositoryTest {

@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); }

@Test void shouldSaveOrder() { // REST-assured test that uses the real database } } ```

This ensures your tests run against the same database dialect as production. The container is reused across tests, but you can clean data between tests using @Sql or @DirtiesContext.

One gotcha: Testcontainers requires Docker. In CI, you need a Docker daemon. Many CI providers support it, but if not, you can fall back to H2. I recommend using a Maven profile to conditionally use Testcontainers.

TestcontainersTest.javaJAVA
1
2
3
4
5
6
@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);
}
Output
Spring Boot uses the PostgreSQL container's JDBC URL, username, and password.
⚠ Container startup time
📊 Production Insight
We once had a bug where H2 accepted invalid data that PostgreSQL rejected. Switching to Testcontainers caught it immediately.
🎯 Key Takeaway
Testcontainers gives you a real database in tests, avoiding H2-specific bugs.
● Production incidentPOST-MORTEMseverity: high

The Silent Port Conflict That Took Down CI

Symptom
CI pipeline would fail intermittently with BindException: Address already in use when running REST-assured tests.
Assumption
Developers assumed it was a resource leak in test setup or teardown.
Root cause
Multiple test classes used @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) without @DirtiesContext, causing the same port to be reused across test suites in parallel runs.
Fix
Added @DirtiesContext(classMode = ClassMode.AFTER_CLASS) to each test class and configured Maven Surefire to fork per test class.
Key lesson
  • Always use random ports in integration tests, never fixed ports.
  • Use @DirtiesContext to reset context between test classes to avoid port conflicts.
  • Configure test execution to fork per class or use unique port ranges.
  • Monitor CI logs for BindExceptions—they are often a sign of port contention.
  • Consider using Testcontainers to manage containerized dependencies, which also helps with port isolation.
Production debug guideSymptom to Action5 entries
Symptom · 01
Test fails with 404 but endpoint exists
Fix
Check base URI and port configuration. Ensure @LocalServerPort is injected correctly.
Symptom · 02
JSON path assertion fails intermittently
Fix
Add logging of the actual response body to see dynamic fields. Use JsonPath with flexible matchers like containsString.
Symptom · 03
Test passes locally but fails in CI
Fix
Check environment variables, database state, and parallel execution settings. Use Testcontainers to replicate the environment.
Symptom · 04
SSL handshake errors in tests
Fix
Configure REST-assured to use relaxed HTTPS validation: given().relaxedHTTPSValidation().
Symptom · 05
Slow test execution
Fix
Profile the test; consider using MockMvc for unit-level tests and REST-assured only for integration scenarios.
★ Quick Debug Cheat SheetRapid diagnostics for common REST-assured issues
BindException on startup
Immediate action
Check for port conflicts
Commands
lsof -i :<port>
ps aux | grep java
Fix now
Use random port: @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
Test fails with 401+
Immediate action
Check if authentication is required
Commands
Print the request headers in test
Verify token/credentials
Fix now
Add auth() to request: given().auth().preemptive().basic(user, pass)
JSON path not found+
Immediate action
Log the actual response body
Commands
given().log().all()
then().log().all()
Fix now
Use flexible matcher: body("key", containsString("expected"))
FeatureREST-assuredMockMvc
HTTP stackReal HTTP (embedded server)Mocked servlet container
SpeedSlower (full context)Faster (lightweight)
Use caseIntegration testsController unit tests
External service mockingWireMock integrationMockito mocks
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
OrderControllerTest.java@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)Setting Up REST-assured in Spring Boot
OrderApiTest.java@TestWriting Your First REST-assured Test
JsonPathAssertionTest.javagiven()Validating JSON Responses with JsonPath
AuthTest.javagiven()Handling Authentication and Headers
AdvancedConfigTest.javagiven()What the Official Docs Won't Tell You
WireMockTest.javastubFor(post(urlEqualTo("/payments"))Mocking External Services with WireMock
TestcontainersTest.java@DynamicPropertySourceIntegrating with Testcontainers for Database Tests

Key takeaways

1
REST-assured provides a fluent DSL for testing REST APIs, integrating seamlessly with Spring Boot.
2
Always use random ports and reset static configuration to avoid test contamination.
3
Combine REST-assured with WireMock and Testcontainers for robust integration tests.
4
Be aware of global state and Spring context caching to prevent flaky tests.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does REST-assured differ from MockMvc in Spring Boot testing?
Q02SENIOR
How do you handle dynamic response fields in REST-assured tests?
Q03SENIOR
Describe a scenario where REST-assured test flakiness occurred and how y...
Q01 of 03SENIOR

How does REST-assured differ from MockMvc in Spring Boot testing?

ANSWER
REST-assured tests against a real HTTP server (embedded Tomcat), while MockMvc mocks the servlet container. REST-assured is better for integration tests that validate the full HTTP stack, including serialization and filters. MockMvc is faster for controller-layer unit tests.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is REST-assured?
02
How do I set up REST-assured with Spring Boot?
03
Can REST-assured test OAuth2 secured endpoints?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

5 min read · try the examples if you haven't

Previous
REST API Pagination in Spring: Pageable, PagedModel, and Custom Strategies
99 / 121 · Spring Boot
Next
Introduction to WireMock: Mocking HTTP Services for Spring REST Testing