REST Assured
The de-facto Java library for readable, given/when/then REST API tests.
What is REST Assured
REST Assured is a Java DSL for testing HTTP APIs. It reads like plain English — given() a request, when() you call an endpoint, then() assert the response — and integrates with JUnit/TestNG, Hamcrest matchers, and JSON/XML path extraction. It is the standard choice on Java/Maven teams.
- Fluent BDD-style syntax that non-Java readers can follow.
- Built-in JSON/XML parsing with JsonPath and Hamcrest matchers.
- First-class auth (Bearer, Basic, OAuth), request/response specs, and schema validation.
- Plays well with existing JUnit 5 / TestNG suites and Maven/Gradle CI.
Target the TestForge API
Base URI http://localhost:3000, base path /api. Endpoints like /api/auth/login, /api/catalog/products, /api/cart/items.
Prerequisites
- JDK 17+ (java -version).
- Maven or Gradle.
- TestForge API reachable at http://localhost:3000.
- Basic Java and JUnit familiarity.
Installation (Maven)
<dependencies>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>Your first test
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class CatalogTest {
@BeforeAll
static void setup() {
RestAssured.baseURI = "http://localhost:3000";
RestAssured.basePath = "/api";
}
@Test
void productsEndpointReturnsData() {
given()
.queryParam("limit", 5)
.when()
.get("/catalog/products")
.then()
.statusCode(200)
.body("data", not(empty()))
.body("data.size()", lessThanOrEqualTo(5))
.body("meta.totalPages", greaterThanOrEqualTo(1));
}
}mvn testThe given / when / then structure
- given(): build the request — headers, query/path params, body, auth.
- when(): perform the HTTP verb — get/post/put/patch/delete on a path.
- then(): assert on the response — statusCode, body (via JsonPath + Hamcrest), headers, time.
body("path", matcher) uses Groovy GPath
The first argument is a JSON path (e.g. "data[0].name" or "items.size()") and the second is a Hamcrest matcher (equalTo, hasItem, greaterThan, ...).
Authentication and request bodies
Log in to get a JWT, then send it as a Bearer token. Serialize request bodies from a Map or POJO.
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
String login() {
return given()
.contentType("application/json")
.body(Map.of("email", "customer@testforge.dev", "password", "Password123!"))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract().path("accessToken");
}
@Test
void addsItemToCart() {
String token = login();
String productId = given().queryParam("limit", 1)
.get("/catalog/products").then().extract().path("data[0].id");
given()
.header("Authorization", "Bearer " + token)
.contentType("application/json")
.body(Map.of("productId", productId, "quantity", 1))
.when()
.post("/cart/items")
.then()
.statusCode(201)
.body("items", not(empty()));
}Extracting values with JsonPath
Use extract() to pull values out of a response for chaining or custom assertions.
import io.restassured.response.Response;
Response res = given().queryParam("limit", 3).get("/catalog/products");
String firstName = res.jsonPath().getString("data[0].name");
int total = res.jsonPath().getInt("meta.total");
List<String> ids = res.jsonPath().getList("data.id");Request and response specifications (DRY)
Extract shared setup (base URI, headers, common assertions) into reusable specs so tests stay concise.
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
RequestSpecification requestSpec = new RequestSpecBuilder()
.setBaseUri("http://localhost:3000").setBasePath("/api")
.setContentType("application/json").build();
ResponseSpecification okJson = new ResponseSpecBuilder()
.expectStatusCode(200).expectContentType("application/json").build();
// Usage: given().spec(requestSpec).get("/catalog/products").then().spec(okJson);JSON schema validation
Assert the whole response conforms to a JSON schema on the classpath — a strong contract test.
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
given().queryParam("limit", 1)
.when().get("/catalog/products")
.then().statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/products.json"));Running in CI
name: api-tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: 'temurin', java-version: '17' }
- run: mvn -B testBest practices
- Use request/response specs to avoid repeating base URI, headers, and common checks.
- Assert exact status codes and specific body fields, including the error.code on failures.
- Keep credentials in environment variables or a config file, never hard-coded in committed tests.
- Log request/response on failure (log().ifValidationFails()) for fast triage.
- Combine with JUnit 5 for parameterized data-driven tests.
JSON schema and response body validation
REST Assured validates a response at two levels: whole-payload contract checks with a JSON schema, and field-level assertions with Groovy GPath plus Hamcrest matchers. Combine both — a schema proves the shape, matchers prove the values.
Whole-response schema validation
Drop a JSON Schema file on the test classpath (src/test/resources) and assert the body matches it with matchesJsonSchemaInClasspath. This catches missing fields, wrong types, and shape drift in a single assertion.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["data", "meta"],
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "name", "price"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"price": { "type": "number" }
}
}
},
"meta": { "type": "object" }
}
}import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
public class SchemaTest {
@Test
void catalogListMatchesSchema() {
given()
.baseUri("http://localhost:3000").basePath("/api")
.queryParam("limit", 5)
.when()
.get("/catalog")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/catalog-list.json"));
}
}Field-level assertions with JSONPath and Hamcrest
The first argument to body() is a GPath expression into the JSON; the second is a Hamcrest matcher. Chain as many as you need — each is checked independently against the same response.
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
given()
.baseUri("http://localhost:3000").basePath("/api")
.when()
.get("/catalog")
.then()
.statusCode(200)
.body("data", not(empty()))
.body("data[0].id", equalTo("prod-001"))
.body("data.price", everyItem(greaterThan(0f)))
.body("data.name", hasItem("Starter Plan"))
.body("data.size()", lessThanOrEqualTo(20));Schema plus matchers, not either/or
Use matchesJsonSchemaInClasspath for structure and types (does every product have a numeric price?) and body("path", matcher) for business values (is the first product priced correctly?). One guards the contract, the other guards the data.
Authentication and specification reuse
TestForge protects its cart and order endpoints with a Bearer JWT. The pattern is: POST credentials to /auth/login, extract the token from the response, then attach it to every subsequent request. Rather than repeating the base URI, content type, and Authorization header in each test, capture them once in a RequestSpecification and reuse it everywhere.
Obtaining and extracting a token
Log in (or register first, then log in) and pull the token out with extract().path(). Keep it in a static field so it is fetched once per test class.
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
String login(String email, String password) {
return given()
.baseUri("http://localhost:3000").basePath("/api")
.contentType("application/json")
.body(Map.of("email", email, "password", password))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.body("accessToken", notNullValue())
.extract().path("accessToken");
}Reusing request and response specs
Build a RequestSpecBuilder once the token is known and pass it to every authenticated call with .spec(). A ResponseSpecBuilder captures the common success assertions so the then() block stays focused on what is unique to each test.
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
public class AuthenticatedTest {
static RequestSpecification authSpec;
static ResponseSpecification okJson;
@BeforeAll
static void setup() {
String token = new AuthenticatedTest()
.login("customer@testforge.dev", "Password123!");
authSpec = new RequestSpecBuilder()
.setBaseUri("http://localhost:3000").setBasePath("/api")
.setContentType("application/json")
.addHeader("Authorization", "Bearer " + token)
.build();
okJson = new ResponseSpecBuilder()
.expectStatusCode(200)
.expectContentType("application/json")
.build();
}
@Test
void addsItemToCart() {
given()
.spec(authSpec)
.body(Map.of("productId", "prod-001", "quantity", 2))
.when()
.post("/cart/items")
.then()
.statusCode(201)
.body("items", not(empty()));
}
String login(String email, String password) {
return given()
.baseUri("http://localhost:3000").basePath("/api")
.contentType("application/json")
.body(Map.of("email", email, "password", password))
.when().post("/auth/login")
.then().statusCode(200).extract().path("accessToken");
}
}One place to change auth
When the token header format or base URI changes, you edit the spec once instead of every test. This is the RequestSpecification/ResponseSpecification payoff: DRY setup and a then() block that only asserts what matters per test.
Serialization and POJOs
Passing Map.of(...) around works for small payloads, but real suites model requests and responses as plain Java objects (POJOs). REST Assured serializes an object you pass to body() into JSON, and deserializes a response into a class with .as(). It uses Jackson automatically when jackson-databind is on the classpath (Gson is the alternative).
Model the request and response
public class CheckoutRequest {
public String paymentMethod;
public String shippingAddress;
public CheckoutRequest() {}
public CheckoutRequest(String paymentMethod, String shippingAddress) {
this.paymentMethod = paymentMethod;
this.shippingAddress = shippingAddress;
}
}import java.util.List;
public class Order {
public String id;
public String status;
public double total;
public List<OrderItem> items;
public static class OrderItem {
public String productId;
public int quantity;
public double unitPrice;
}
}Send a POJO, deserialize the response
Hand the request object straight to body() — REST Assured serializes it. Call .extract().as(Order.class) to bind the JSON response back into a typed object you can assert on with plain Java.
import model.CheckoutRequest;
import model.Order;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.*;
CheckoutRequest body =
new CheckoutRequest("card", "1 Test Street, QA City");
Order order =
given()
.spec(authSpec)
.body(body)
.when()
.post("/orders/checkout")
.then()
.statusCode(201)
.extract().as(Order.class);
assertNotNull(order.id);
assertEquals("confirmed", order.status);
assertTrue(order.total > 0);
assertFalse(order.items.isEmpty());POJOs make assertions type-safe
Deserializing with .as() moves you from stringly-typed GPath ("data[0].total") to compiler-checked fields (order.total). Reserve POJOs for payloads you assert on heavily; keep GPath/matchers for quick, one-off field checks.
Interview questions and answers
Walk me through the given/when/then structure in REST Assured. What belongs in each block?
given() is where you build the request — base URI, headers, query and path params, authentication, and the body. when() performs the HTTP verb (get, post, put, patch, delete) against a path. then() runs assertions on the response: status code, headers, body via GPath and Hamcrest, and response time. The three-part shape maps directly onto BDD, which is why non-Java teammates can still read the tests.
How do you extract a value from a response for use later in the test?
You end the chain with .extract() and then pull the piece you want — .extract().path("accessToken") for a single field, .extract().jsonPath().getList("data.id") for a list, or .extract().response() for the whole Response object. A common pattern is extracting a token or an id from one call and feeding it into the next, for example grabbing accessToken from /auth/login and attaching it as a Bearer header on a /cart/items request.
What is JSON schema validation and when would you use it over field assertions?
Schema validation checks the entire response against a JSON Schema document with matchesJsonSchemaInClasspath("schemas/foo.json"), asserting structure, required fields, and types in one shot. You reach for it as a contract test — it catches a missing field or a type change anywhere in the payload without listing every field by hand. Field-level body("path", matcher) assertions are for specific business values; the two are complementary, not competing.
What are Hamcrest matchers and why does REST Assured lean on them?
Hamcrest matchers are composable predicates like equalTo, hasItem, greaterThan, not, empty, and everyItem that produce readable, self-describing assertions and clear failure messages. In REST Assured they are the second argument to body(), applied to a GPath expression — for example body("data.price", everyItem(greaterThan(0f))). Because they compose, you can express things like not(empty()) or hasItems(...) without writing custom assertion logic.
How do you handle authentication and tokens in a REST Assured suite?
For a JWT API like TestForge you POST credentials to /auth/login, extract the token with .extract().path("accessToken"), and send it on protected calls as .header("Authorization", "Bearer " + token). REST Assured also has built-in auth helpers (.auth().oauth2(token), basic, preemptive) for the common schemes. Fetch the token once per class in @BeforeAll and reuse it rather than logging in on every test.
Explain RequestSpecification and ResponseSpecification. Why use them?
A RequestSpecification (built with RequestSpecBuilder) bundles reusable request setup — base URI, content type, common headers, the auth token — so each test just calls given().spec(requestSpec). A ResponseSpecification (ResponseSpecBuilder) captures shared response expectations like expectStatusCode(200) and content type. They keep tests DRY and give you a single place to update when the base URI or auth header format changes, instead of editing every test.
How does POJO serialization and deserialization work in REST Assured?
If Jackson (or Gson) is on the classpath, REST Assured serializes any object you pass to body() into JSON automatically, so you can hand it a CheckoutRequest instead of a hand-built Map. On the response side, .extract().as(Order.class) deserializes the JSON back into a typed object you can assert on with plain Java. This moves you from stringly-typed GPath to compiler-checked fields, which is worth it for payloads you assert on heavily.
How does REST Assured integrate with TestNG or JUnit?
REST Assured is just a library, so it runs inside whatever test runner you use — the given/when/then chain lives in a @Test method under JUnit 5 or TestNG. You keep the runner features you already have: @BeforeAll/@BeforeClass for setup like login, parameterized or data-driven tests (@ParameterizedTest, TestNG @DataProvider), grouping, and parallel execution. REST Assured does not replace the runner; it supplies the HTTP-and-assertion DSL that the runner drives.
How do you debug a failing test — what logging and filter options exist?
The quickest tool is log().ifValidationFails() on the then() block (or log().all() on given()) which prints the request and response only when an assertion fails, keeping passing runs quiet. For cross-cutting concerns you attach Filters — RequestLoggingFilter and ResponseLoggingFilter for full request/response logging, or a custom Filter to inject a trace id or capture timings on every call. Filters run around each request, so they are the right place for anything you want applied suite-wide.
How do you validate status codes, headers, and the body of a response?
Status codes go through .statusCode(200) or a matcher like statusCode(both(greaterThanOrEqualTo(200)).and(lessThan(300))). Headers use .header("Content-Type", containsString("application/json")) or .contentType(ContentType.JSON). Body assertions use body("gpath", hamcrestMatcher) for individual fields and matchesJsonSchemaInClasspath for the whole payload. Because these all chain in one then() block, a single call can assert the status, the headers, and multiple body fields together.
Where to go next
- Docs: rest-assured.io and the usage guide on GitHub.
- Learn Hamcrest matchers and Groovy GPath deeply — they unlock expressive assertions.
- Practice: automate the TestForge auth flow (login, refresh, protected routes) and the order lifecycle end to end.