Home / Blog / REST Assured Tutorial: Build a Complete API Test Framework in Java
REST Assured Tutorial: Build a Complete API Test Framework in Java
REST Assured is the standard Java library for API test automation. It provides a fluent, readable DSL (Domain-Specific Language) for making HTTP requests and validating responses. If you are targeting SDET roles that use Java, REST Assured is a non-negotiable skill.
Setup: Maven Dependencies
Add to your pom.xml:
<dependencies>
<!-- REST Assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<!-- JSON Schema Validation -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.9.0</version>
<scope>test</scope>
</dependency>
<!-- Jackson for JSON serialisation -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
</dependencies>Your First REST Assured Test
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.testng.annotations.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class FirstApiTest {
@Test
public void testGetUser() {
given()
.baseUri("https://jsonplaceholder.typicode.com")
.when()
.get("/users/1")
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("name", notNullValue())
.body("email", containsString("@"));
}
}This is REST Assured's fluent syntax: given() sets up the request, when() sends it, then() validates the response.
The given/when/then Pattern
given()
// Request setup
.baseUri("https://api.example.com")
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.queryParam("page", 1)
.queryParam("size", 10)
.when()
// HTTP method and path
.get("/users")
.then()
// Response assertions
.statusCode(200)
.time(lessThan(2000L)) // Response under 2 seconds
.body("data.size()", greaterThan(0))
.body("data[0].id", notNullValue());Base Configuration — API Test Base Class
// src/test/java/com/qaknowledgehub/base/ApiTestBase.java
package com.qaknowledgehub.base;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.testng.annotations.BeforeClass;
import static org.hamcrest.Matchers.lessThan;
public class ApiTestBase {
protected static RequestSpecification requestSpec;
protected static ResponseSpecification responseSpec;
@BeforeClass
public static void setup() {
RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
requestSpec = new RequestSpecBuilder()
.setContentType("application/json")
.addFilter(new RequestLoggingFilter()) // Log all requests
.addFilter(new ResponseLoggingFilter()) // Log all responses
.build();
responseSpec = new ResponseSpecBuilder()
.expectResponseTime(lessThan(3000L)) // All responses < 3s
.build();
}
}GET Requests — Fetching Data
// src/test/java/com/qaknowledgehub/tests/GetUserTests.java
package com.qaknowledgehub.tests;
import com.qaknowledgehub.base.ApiTestBase;
import io.restassured.response.Response;
import org.testng.Assert;
import org.testng.annotations.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
public class GetUserTests extends ApiTestBase {
@Test(description = "GET /users/1 returns correct user data")
public void testGetUserById() {
given()
.spec(requestSpec)
.when()
.get("/users/1")
.then()
.spec(responseSpec)
.statusCode(200)
.body("id", equalTo(1))
.body("name", equalTo("Leanne Graham"))
.body("email", equalTo("Sincere@april.biz"));
}
@Test(description = "GET /users returns a list of 10 users")
public void testGetAllUsers() {
given()
.spec(requestSpec)
.when()
.get("/users")
.then()
.statusCode(200)
.body("size()", equalTo(10))
.body("[0].id", notNullValue())
.body("[0].name", notNullValue());
}
@Test(description = "GET /users/9999 returns 404 for non-existent user")
public void testGetNonExistentUser() {
given()
.spec(requestSpec)
.when()
.get("/users/9999")
.then()
.statusCode(404);
}
@Test(description = "Extract and use response body values")
public void testExtractResponseValues() {
Response response = given()
.spec(requestSpec)
.when()
.get("/users/1")
.then()
.statusCode(200)
.extract()
.response();
String name = response.jsonPath().getString("name");
String email = response.jsonPath().getString("email");
int userId = response.jsonPath().getInt("id");
Assert.assertEquals(userId, 1);
Assert.assertNotNull(name);
Assert.assertTrue(email.contains("@"), "Email should contain @");
}
}POST Requests — Creating Resources
Using HashMap
@Test(description = "POST /posts creates a new post")
public void testCreatePost() {
Map<String, Object> payload = new HashMap<>();
payload.put("title", "My Test Post");
payload.put("body", "This is the post body.");
payload.put("userId", 1);
given()
.spec(requestSpec)
.body(payload)
.when()
.post("/posts")
.then()
.statusCode(201)
.body("title", equalTo("My Test Post"))
.body("id", notNullValue());
}Using POJO (Plain Old Java Object)
// src/main/java/com/qaknowledgehub/models/Post.java
public class Post {
private String title;
private String body;
private int userId;
public Post(String title, String body, int userId) {
this.title = title;
this.body = body;
this.userId = userId;
}
// Getters (required for Jackson serialisation)
public String getTitle() { return title; }
public String getBody() { return body; }
public int getUserId() { return userId; }
}@Test
public void testCreatePostWithPojo() {
Post newPost = new Post("Interview Prep Guide", "Complete guide to QA interviews", 1);
given()
.spec(requestSpec)
.body(newPost)
.when()
.post("/posts")
.then()
.statusCode(201)
.body("title", equalTo("Interview Prep Guide"))
.body("userId", equalTo(1));
}PUT and PATCH Requests
@Test(description = "PUT /posts/1 replaces the entire post")
public void testUpdatePostWithPut() {
Map<String, Object> updatedPost = new HashMap<>();
updatedPost.put("id", 1);
updatedPost.put("title", "Updated Title");
updatedPost.put("body", "Updated body content.");
updatedPost.put("userId", 1);
given()
.spec(requestSpec)
.body(updatedPost)
.when()
.put("/posts/1")
.then()
.statusCode(200)
.body("title", equalTo("Updated Title"));
}
@Test(description = "PATCH /posts/1 updates only specified fields")
public void testPartialUpdateWithPatch() {
Map<String, String> patchPayload = new HashMap<>();
patchPayload.put("title", "Only the Title Changed");
given()
.spec(requestSpec)
.body(patchPayload)
.when()
.patch("/posts/1")
.then()
.statusCode(200)
.body("title", equalTo("Only the Title Changed"));
}DELETE Requests
@Test(description = "DELETE /posts/1 removes the post")
public void testDeletePost() {
given()
.spec(requestSpec)
.when()
.delete("/posts/1")
.then()
.statusCode(200); // JSONPlaceholder returns 200 for DELETE
}Authentication
Bearer Token
String token = "eyJhbGciOiJIUzI1NiJ9..."; // From login response
given()
.header("Authorization", "Bearer " + token)
.spec(requestSpec)
.when()
.get("/profile")
.then()
.statusCode(200);Basic Auth
given()
.auth().basic("username", "password")
.spec(requestSpec)
.when()
.get("/admin/users")
.then()
.statusCode(200);OAuth 2.0 Flow (Login → Use Token)
@Test
public void testAuthenticatedEndpoint() {
// Step 1: Login and extract token
String token = given()
.spec(requestSpec)
.body(Map.of("username", "user1", "password", "pass123"))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.jsonPath()
.getString("token");
Assert.assertNotNull(token, "Token should not be null");
// Step 2: Use token for authenticated request
given()
.spec(requestSpec)
.header("Authorization", "Bearer " + token)
.when()
.get("/profile")
.then()
.statusCode(200)
.body("username", equalTo("user1"));
}Query Parameters and Path Parameters
// Query parameters: /users?page=1&size=20
given()
.spec(requestSpec)
.queryParam("page", 1)
.queryParam("size", 20)
.when()
.get("/users")
.then()
.statusCode(200);
// Path parameters: /users/{userId}/orders/{orderId}
given()
.spec(requestSpec)
.pathParam("userId", 42)
.pathParam("orderId", 1001)
.when()
.get("/users/{userId}/orders/{orderId}")
.then()
.statusCode(200);JSON Schema Validation
Schema validation verifies the response structure (field names, types, required fields) — not just field values.
Create src/test/resources/schemas/user_schema.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "name", "email", "phone"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string", "minLength": 1 },
"email": { "type": "string", "format": "email" },
"phone": { "type": "string" }
}
}import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
@Test
public void testUserResponseMatchesSchema() {
given()
.spec(requestSpec)
.when()
.get("/users/1")
.then()
.statusCode(200)
.body(matchesJsonSchemaInClasspath("schemas/user_schema.json"));
}This catches contract breaks — if a field is renamed or a required field is removed, the schema validation fails immediately.
Data-Driven Testing with TestNG DataProvider
@DataProvider(name = "userIds")
public Object[][] provideUserIds() {
return new Object[][] {
{1, 200},
{5, 200},
{10, 200},
{9999, 404}
};
}
@Test(dataProvider = "userIds")
public void testGetUserWithVariousIds(int userId, int expectedStatus) {
given()
.spec(requestSpec)
.when()
.get("/users/" + userId)
.then()
.statusCode(expectedStatus);
}Complete Test Example: CRUD Operations Chain
public class CrudOperationsTest extends ApiTestBase {
private static int createdPostId;
@Test(priority = 1, description = "Create a new post")
public void testCreatePost() {
Map<String, Object> payload = Map.of(
"title", "CRUD Test Post",
"body", "Testing create operation",
"userId", 1
);
createdPostId = given()
.spec(requestSpec)
.body(payload)
.when()
.post("/posts")
.then()
.statusCode(201)
.body("title", equalTo("CRUD Test Post"))
.extract()
.jsonPath()
.getInt("id");
Assert.assertTrue(createdPostId > 0, "Post ID should be positive");
}
@Test(priority = 2, description = "Read the created post", dependsOnMethods = "testCreatePost")
public void testReadCreatedPost() {
// JSONPlaceholder doesn't persist, so we read an existing post
given()
.spec(requestSpec)
.when()
.get("/posts/1")
.then()
.statusCode(200)
.body("id", equalTo(1));
}
@Test(priority = 3, description = "Update the post")
public void testUpdatePost() {
given()
.spec(requestSpec)
.body(Map.of("title", "Updated Post Title"))
.when()
.patch("/posts/1")
.then()
.statusCode(200)
.body("title", equalTo("Updated Post Title"));
}
@Test(priority = 4, description = "Delete the post")
public void testDeletePost() {
given()
.spec(requestSpec)
.when()
.delete("/posts/1")
.then()
.statusCode(200);
}
}Common REST Assured Hamcrest Matchers
// Equality
.body("status", equalTo("success"))
.body("count", equalTo(10))
// Null checks
.body("data", notNullValue())
.body("error", nullValue())
// String matchers
.body("message", containsString("successfully"))
.body("email", endsWith("@example.com"))
.body("url", startsWith("https://"))
// Numeric comparisons
.body("count", greaterThan(0))
.body("price", lessThanOrEqualTo(999.99f))
// Collection matchers
.body("tags", hasSize(3))
.body("tags", hasItems("qa", "automation"))
.body("ids", everyItem(greaterThan(0)))
// Type checking
.body("id", instanceOf(Integer.class))Summary
You now have a complete REST Assured framework with:
- Base request/response specifications (no duplication across tests)
- GET, POST, PUT, PATCH, DELETE test coverage
- Authentication patterns (Bearer token, Basic Auth)
- JSON Schema validation for contract testing
- Data-driven tests with TestNG
@DataProvider - CRUD operation chaining
This framework structure is exactly what SDET interviewers expect to see in a portfolio project. Push it to GitHub, add 20+ tests, and you have a strong interview talking point.
Recommended Resource
API Testing Notes Pack
Structured API testing notes for interview and practical project execution.
Related Posts
35 Playwright Interview Questions (With Answers)
Playwright interview questions and answers for QA and SDET roles — covering setup, locators, waits, fixtures, API testing, and debugging.
Read article →Page Object Model in Selenium: Design and Best Practices
Learn the Page Object Model design pattern for Selenium — why it exists, how to implement it correctly in Java, and the mistakes to avoid.
Read article →60 Selenium Interview Questions for Java Developers
The most asked Selenium + Java interview questions with complete answers — covering WebDriver setup, locators, waits, POM, TestNG, and framework design.
Read article →