Introduction
REST Assured is a powerful Java DSL for testing RESTful APIs. Its fluent interface makes writing and maintaining API tests simple and readable. This guide covers comprehensive implementation of REST Assured for API testing, including advanced patterns, customizations, and best practices.
Architecture Overview
[Test Cases] → [REST Assured DSL] → [HTTP Client] → [API Server] → [Response Validation] ↓ ↓ ↓ ↓ ↓ JUnit 5 Fluent API HTTP Request Endpoints JSON Schema TestNG Given/When/Then Headers Business Logic Status Codes Cucumber Request Spec Authentication Data Access Response Body
Step 1: Project Setup and Dependencies
Maven Configuration
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>rest-assured-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project-build.sourceEncoding>
<!-- Testing -->
<rest-assured.version>5.3.2</rest-assured.version>
<junit.version>5.10.0</junit.version>
<assertj.version>3.24.2</assertj.version>
<jackson.version>2.15.0</jackson.version>
<json-schema-validator.version>5.3.2</json-schema-validator.version>
<faker.version>1.0.2</faker.version>
<allure.version>2.22.0</allure.version>
</properties>
<dependencies>
<!-- REST Assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-path</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>xml-path</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Test Data -->
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>${faker.version}</version>
<scope>test</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<!-- Allure Reporting -->
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-rest-assured</artifactId>
<version>${allure.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>${allure.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<systemPropertyVariables>
<allure.results.directory>target/allure-results</allure.results.directory>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.12.0</version>
</plugin>
</plugins>
</build>
</project>
Step 2: Test Data Models
Request/Response Models
User.java
package com.example.restassured.models;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private Long id;
private String name;
private String email;
private String phone;
private String website;
@JsonProperty("created_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime updatedAt;
private Address address;
private Company company;
private List<String> tags;
// Constructors
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getWebsite() { return website; }
public void setWebsite(String website) { this.website = website; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public Address getAddress() { return address; }
public void setAddress(Address address) { this.address = address; }
public Company getCompany() { return company; }
public void setCompany(Company company) { this.company = company; }
public List<String> getTags() { return tags; }
public void setTags(List<String> tags) { this.tags = tags; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name) &&
Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(id, name, email);
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
// Builder pattern
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final User user = new User();
public Builder id(Long id) {
user.setId(id);
return this;
}
public Builder name(String name) {
user.setName(name);
return this;
}
public Builder email(String email) {
user.setEmail(email);
return this;
}
public Builder phone(String phone) {
user.setPhone(phone);
return this;
}
public Builder website(String website) {
user.setWebsite(website);
return this;
}
public Builder address(Address address) {
user.setAddress(address);
return this;
}
public Builder company(Company company) {
user.setCompany(company);
return this;
}
public Builder tags(List<String> tags) {
user.setTags(tags);
return this;
}
public User build() {
return user;
}
}
}
Address.java
package com.example.restassured.models;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Address {
private String street;
private String suite;
private String city;
@JsonProperty("zipcode")
private String zipCode;
private Geo geo;
// Getters and Setters
public String getStreet() { return street; }
public void setStreet(String street) { this.street = street; }
public String getSuite() { return suite; }
public void setSuite(String suite) { this.suite = suite; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getZipCode() { return zipCode; }
public void setZipCode(String zipCode) { this.zipCode = zipCode; }
public Geo getGeo() { return geo; }
public void setGeo(Geo geo) { this.geo = geo; }
public static class Builder {
private final Address address = new Address();
public Builder street(String street) {
address.setStreet(street);
return this;
}
public Builder suite(String suite) {
address.setSuite(suite);
return this;
}
public Builder city(String city) {
address.setCity(city);
return this;
}
public Builder zipCode(String zipCode) {
address.setZipCode(zipCode);
return this;
}
public Builder geo(Geo geo) {
address.setGeo(geo);
return this;
}
public Address build() {
return address;
}
}
}
ApiResponse.java
package com.example.restassured.models;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private List<String> errors;
@JsonProperty("status_code")
private int statusCode;
private Pagination pagination;
// Getters and Setters
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public List<String> getErrors() { return errors; }
public void setErrors(List<String> errors) { this.errors = errors; }
public int getStatusCode() { return statusCode; }
public void setStatusCode(int statusCode) { this.statusCode = statusCode; }
public Pagination getPagination() { return pagination; }
public void setPagination(Pagination pagination) { this.pagination = pagination; }
public static class Pagination {
private int page;
private int size;
private int total;
@JsonProperty("total_pages")
private int totalPages;
// Getters and Setters
public int getPage() { return page; }
public void setPage(int page) { this.page = page; }
public int getSize() { return size; }
public void setSize(int size) { this.size = size; }
public int getTotal() { return total; }
public void setTotal(int total) { this.total = total; }
public int getTotalPages() { return totalPages; }
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
}
}
Step 3: Configuration and Setup
Test Configuration
TestConfig.java
package com.example.restassured.config;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.config.*;
import io.restassured.filter.log.LogDetail;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import io.restassured.http.ContentType;
import io.restassured.parsing.Parser;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.junit.jupiter.api.BeforeAll;
import java.util.concurrent.TimeUnit;
import static io.restassured.config.DecoderConfig.decoderConfig;
import static io.restassured.config.EncoderConfig.encoderConfig;
import static io.restassured.config.JsonConfig.jsonConfig;
import static io.restassured.config.RestAssuredConfig.newConfig;
import static io.restassured.path.json.config.JsonPathConfig.NumberReturnType.BIG_DECIMAL;
public class TestConfig {
public static final String BASE_URL = "https://jsonplaceholder.typicode.com";
public static final String API_VERSION = "/v1";
public static final int DEFAULT_TIMEOUT = 30000;
@BeforeAll
public static void setup() {
// Global REST Assured configuration
RestAssured.config = newConfig()
.jsonConfig(jsonConfig().numberReturnType(BIG_DECIMAL))
.encoderConfig(encoderConfig().appendDefaultContentCharsetToContentTypeIfUndefined(false))
.decoderConfig(decoderConfig().defaultContentCharset("UTF-8"))
.redirect(redirectConfig().followRedirects(true).maxRedirects(3))
.sessionConfig(sessionConfig().sessionIdName("JSESSIONID"))
.logConfig(logConfig().enableLoggingOfRequestAndResponseIfValidationFails(LogDetail.ALL));
// Default base URI
RestAssured.baseURI = BASE_URL;
// Register custom parsers if needed
RestAssured.registerParser("text/plain", Parser.JSON);
// Enable logging for all requests if in debug mode
if (Boolean.parseBoolean(System.getProperty("debug", "false"))) {
RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());
}
}
public static RequestSpecification getRequestSpec() {
return new RequestSpecBuilder()
.setBaseUri(BASE_URL)
.setBasePath(API_VERSION)
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.addHeader("User-Agent", "REST-Assured-API-Tests/1.0")
.addFilter(new RequestLoggingFilter())
.addFilter(new ResponseLoggingFilter())
.setConfig(newConfig()
.httpClient(HttpClientConfig.httpClientConfig()
.setParam("http.connection.timeout", DEFAULT_TIMEOUT)
.setParam("http.socket.timeout", DEFAULT_TIMEOUT)))
.build();
}
public static RequestSpecification getRequestSpecWithAuth(String token) {
return new RequestSpecBuilder()
.addRequestSpecification(getRequestSpec())
.addHeader("Authorization", "Bearer " + token)
.build();
}
public static ResponseSpecification getResponseSpec() {
return new ResponseSpecBuilder()
.expectContentType(ContentType.JSON)
.expectHeader("Content-Type", "application/json; charset=utf-8")
.expectResponseTime(org.hamcrest.Matchers.lessThan(5000L))
.build();
}
public static ResponseSpecification getResponseSpec(int expectedStatusCode) {
return new ResponseSpecBuilder()
.addResponseSpecification(getResponseSpec())
.expectStatusCode(expectedStatusCode)
.build();
}
public static ResponseSpecification getCreatedResponseSpec() {
return getResponseSpec(201);
}
public static ResponseSpecification getSuccessResponseSpec() {
return getResponseSpec(200);
}
public static ResponseSpecification getNotFoundResponseSpec() {
return getResponseSpec(404);
}
public static ResponseSpecification getBadRequestResponseSpec() {
return getResponseSpec(400);
}
public static ResponseSpecification getUnauthorizedResponseSpec() {
return getResponseSpec(401);
}
}
Test Data Factory
TestDataFactory.java
package com.example.restassured.factory;
import com.example.restassured.models.Address;
import com.example.restassured.models.User;
import com.github.javafaker.Faker;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class TestDataFactory {
private static final Faker faker = new Faker(Locale.ENGLISH);
private TestDataFactory() {
// Utility class
}
public static User createValidUser() {
return User.builder()
.name(faker.name().fullName())
.email(faker.internet().emailAddress())
.phone(faker.phoneNumber().phoneNumber())
.website(faker.internet().url())
.address(createAddress())
.tags(Arrays.asList("customer", "premium"))
.build();
}
public static User createUserWithName(String name) {
User user = createValidUser();
user.setName(name);
return user;
}
public static User createUserWithEmail(String email) {
User user = createValidUser();
user.setEmail(email);
return user;
}
public static User createInvalidUser() {
return User.builder()
.name("") // Empty name
.email("invalid-email") // Invalid email
.build();
}
public static Address createAddress() {
return new Address.Builder()
.street(faker.address().streetAddress())
.city(faker.address().city())
.suite(faker.address().buildingNumber())
.zipCode(faker.address().zipCode())
.build();
}
public static List<User> createUserList(int count) {
return java.util.stream.IntStream.range(0, count)
.mapToObj(i -> createValidUser())
.toList();
}
public static String randomEmail() {
return faker.internet().emailAddress();
}
public static String randomName() {
return faker.name().fullName();
}
public static String randomPhone() {
return faker.phoneNumber().phoneNumber();
}
}
Step 4: Base Test Class
Abstract Base Test
BaseApiTest.java
package com.example.restassured.base;
import com.example.restassured.config.TestConfig;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.qameta.allure.restassured.AllureRestAssured;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.lessThan;
public abstract class BaseApiTest {
protected static final ObjectMapper objectMapper = new ObjectMapper();
protected RequestSpecification requestSpec;
@BeforeEach
public void setUp() {
// Initialize request specification with Allure reporting
requestSpec = new RequestSpecBuilder()
.addSpec(TestConfig.getRequestSpec())
.addFilter(new AllureRestAssured())
.build();
RestAssured.requestSpecification = requestSpec;
}
// Common HTTP methods with fluent API
protected Response get(String path) {
return given()
.spec(requestSpec)
.when()
.get(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response get(String path, Map<String, Object> queryParams) {
return given()
.spec(requestSpec)
.queryParams(queryParams)
.when()
.get(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response get(String path, String pathParam, Object value) {
return given()
.spec(requestSpec)
.pathParam(pathParam, value)
.when()
.get(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response post(String path, Object body) {
return given()
.spec(requestSpec)
.body(serializeToJson(body))
.when()
.post(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response put(String path, Object body) {
return given()
.spec(requestSpec)
.body(serializeToJson(body))
.when()
.put(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response patch(String path, Object body) {
return given()
.spec(requestSpec)
.body(serializeToJson(body))
.when()
.patch(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response delete(String path) {
return given()
.spec(requestSpec)
.when()
.delete(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
protected Response delete(String path, String pathParam, Object value) {
return given()
.spec(requestSpec)
.pathParam(pathParam, value)
.when()
.delete(path)
.then()
.time(lessThan(5000L))
.extract()
.response();
}
// Utility methods
protected String serializeToJson(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize object to JSON", e);
}
}
protected <T> T deserializeFromJson(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize JSON to object", e);
}
}
protected <T> T extractFromResponse(Response response, String jsonPath, Class<T> clazz) {
return response.jsonPath().getObject(jsonPath, clazz);
}
protected void validateJsonSchema(Response response, String schemaPath) {
response.then()
.assertThat()
.body(io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath(schemaPath));
}
}
Step 5: Comprehensive Test Examples
1. Basic CRUD Operations Tests
UserApiTest.java
package com.example.restassured.tests;
import com.example.restassured.base.BaseApiTest;
import com.example.restassured.factory.TestDataFactory;
import com.example.restassured.models.User;
import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.*;
class UserApiTest extends BaseApiTest {
private static final String USERS_PATH = "/users";
private static final String USER_BY_ID_PATH = "/users/{id}";
@Test
@DisplayName("GET /users - Should retrieve all users")
void shouldRetrieveAllUsers() {
Response response = get(USERS_PATH);
response.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("$", hasSize(greaterThan(0)))
.body("[0].id", notNullValue())
.body("[0].name", notNullValue())
.body("[0].email", notNullValue());
List<User> users = response.jsonPath().getList("", User.class);
assertThat(users).isNotEmpty();
assertThat(users.get(0).getId()).isPositive();
}
@Test
@DisplayName("GET /users/{id} - Should retrieve user by ID")
void shouldRetrieveUserById() {
int userId = 1;
Response response = get(USER_BY_ID_PATH, "id", userId);
response.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("id", equalTo(userId))
.body("name", not(emptyOrNullString()))
.body("email", containsString("@"))
.body("address.city", not(emptyOrNullString()));
User user = response.as(User.class);
assertThat(user.getId()).isEqualTo(userId);
assertThat(user.getName()).isNotBlank();
assertThat(user.getEmail()).contains("@");
}
@Test
@DisplayName("GET /users/{id} - Should return 404 for non-existent user")
void shouldReturnNotFoundForNonExistentUser() {
int nonExistentUserId = 9999;
Response response = get(USER_BY_ID_PATH, "id", nonExistentUserId);
response.then()
.spec(TestConfig.getNotFoundResponseSpec());
}
@Test
@DisplayName("POST /users - Should create new user")
void shouldCreateNewUser() {
User newUser = TestDataFactory.createValidUser();
Response response = post(USERS_PATH, newUser);
response.then()
.spec(TestConfig.getCreatedResponseSpec())
.body("id", notNullValue())
.body("name", equalTo(newUser.getName()))
.body("email", equalTo(newUser.getEmail()))
.body("phone", equalTo(newUser.getPhone()))
.body("website", equalTo(newUser.getWebsite()));
User createdUser = response.as(User.class);
assertThat(createdUser.getId()).isPositive();
assertThat(createdUser.getName()).isEqualTo(newUser.getName());
assertThat(createdUser.getEmail()).isEqualTo(newUser.getEmail());
}
@Test
@DisplayName("PUT /users/{id} - Should update existing user")
void shouldUpdateExistingUser() {
int userId = 1;
User updatedUser = TestDataFactory.createValidUser();
Response response = put(USER_BY_ID_PATH, updatedUser)
.pathParam("id", userId);
response.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("id", equalTo(userId))
.body("name", equalTo(updatedUser.getName()))
.body("email", equalTo(updatedUser.getEmail()));
User user = response.as(User.class);
assertThat(user.getId()).isEqualTo(userId);
assertThat(user.getName()).isEqualTo(updatedUser.getName());
}
@Test
@DisplayName("PATCH /users/{id} - Should partially update user")
void shouldPartiallyUpdateUser() {
int userId = 1;
Map<String, Object> partialUpdate = Map.of(
"name", TestDataFactory.randomName(),
"email", TestDataFactory.randomEmail()
);
Response response = given()
.spec(requestSpec)
.body(partialUpdate)
.when()
.patch(USER_BY_ID_PATH, userId)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("id", equalTo(userId))
.body("name", equalTo(partialUpdate.get("name")))
.body("email", equalTo(partialUpdate.get("email")))
.extract()
.response();
User user = response.as(User.class);
assertThat(user.getName()).isEqualTo(partialUpdate.get("name"));
assertThat(user.getEmail()).isEqualTo(partialUpdate.get("email"));
}
@Test
@DisplayName("DELETE /users/{id} - Should delete user")
void shouldDeleteUser() {
// Note: JSONPlaceholder is a fake API, so deletion doesn't actually happen
int userId = 1;
Response response = delete(USER_BY_ID_PATH, "id", userId);
response.then()
.spec(TestConfig.getSuccessResponseSpec());
}
@Test
@DisplayName("GET /users - Should support query parameters")
void shouldSupportQueryParameters() {
Map<String, Object> queryParams = Map.of(
"_page", 1,
"_limit", 5
);
Response response = get(USERS_PATH, queryParams);
response.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("$", hasSize(lessThanOrEqualTo(5)));
}
}
2. Advanced Validation Tests
UserValidationTest.java
package com.example.restassured.tests;
import com.example.restassured.base.BaseApiTest;
import com.example.restassured.factory.TestDataFactory;
import com.example.restassured.models.User;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.*;
class UserValidationTest extends BaseApiTest {
private static final String USERS_PATH = "/users";
@Test
@DisplayName("Should validate response structure using JSON Schema")
void shouldValidateResponseStructure() {
given()
.spec(requestSpec)
.when()
.get(USERS_PATH)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body(matchesJsonSchemaInClasspath("schemas/users-schema.json"));
}
@Test
@DisplayName("Should validate specific user response structure")
void shouldValidateUserResponseStructure() {
given()
.spec(requestSpec)
.when()
.get("/users/{id}", 1)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body(matchesJsonSchemaInClasspath("schemas/user-schema.json"));
}
@Test
@DisplayName("Should validate response headers")
void shouldValidateResponseHeaders() {
given()
.spec(requestSpec)
.when()
.get(USERS_PATH)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.header("Content-Type", containsString(ContentType.JSON.toString()))
.header("Content-Encoding", notNullValue())
.header("Connection", equalTo("keep-alive"));
}
@Test
@DisplayName("Should validate response time")
void shouldValidateResponseTime() {
given()
.spec(requestSpec)
.when()
.get(USERS_PATH)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.time(lessThan(3000L)); // Response time less than 3 seconds
}
@Test
@DisplayName("Should validate user data types and formats")
void shouldValidateUserDataTypesAndFormats() {
given()
.spec(requestSpec)
.when()
.get("/users/{id}", 1)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("id", instanceOf(Integer.class))
.body("name", instanceOf(String.class))
.body("email", matchesPattern("^[A-Za-z0-9+_.-]+@(.+)$")) // Email pattern
.body("phone", matchesPattern("^[0-9\\-\\(\\)\\s]+$")) // Phone pattern
.body("website", matchesPattern("^https?://.+$")); // URL pattern
}
@Test
@DisplayName("Should validate array elements")
void shouldValidateArrayElements() {
given()
.spec(requestSpec)
.when()
.get(USERS_PATH)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("$", hasSize(greaterThan(0))) // Array is not empty
.body("id", everyItem(notNullValue())) // Every item has id
.body("name", everyItem(not(emptyOrNullString()))) // Every item has name
.body("email", everyItem(containsString("@"))); // Every item has valid email format
}
@Test
@DisplayName("Should validate nested objects")
void shouldValidateNestedObjects() {
given()
.spec(requestSpec)
.when()
.get("/users/{id}", 1)
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("address.street", not(emptyOrNullString()))
.body("address.city", not(emptyOrNullString()))
.body("address.zipcode", not(emptyOrNullString()))
.body("address.geo.lat", not(emptyOrNullString()))
.body("address.geo.lng", not(emptyOrNullString()));
}
@Test
@DisplayName("Should validate response using JSONPath")
void shouldValidateResponseUsingJsonPath() {
Response response = get("/users/1");
response.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("id", equalTo(1))
.body("name", equalTo("Leanne Graham"))
.body("username", equalTo("Bret"))
.body("address.city", equalTo("Gwenborough"))
.body("company.name", equalTo("Romaguera-Crona"));
}
@Test
@DisplayName("Should extract and validate multiple values")
void shouldExtractAndValidateMultipleValues() {
Response response = get(USERS_PATH);
// Extract multiple values and validate
List<Integer> userIds = response.jsonPath().getList("id");
List<String> userNames = response.jsonPath().getList("name");
List<String> userEmails = response.jsonPath().getList("email");
assertThat(userIds).allMatch(id -> id > 0);
assertThat(userNames).allMatch(name -> !name.isEmpty());
assertThat(userEmails).allMatch(email -> email.contains("@"));
}
@Test
@DisplayName("Should validate error response for invalid data")
void shouldValidateErrorResponseForInvalidData() {
User invalidUser = TestDataFactory.createInvalidUser();
given()
.spec(requestSpec)
.body(invalidUser)
.when()
.post(USERS_PATH)
.then()
.statusCode(anyOf(equalTo(400), equalTo(422))) // Could be either
.body("$", hasKey("errors")) // Should have errors field
.body("errors", hasSize(greaterThan(0))); // Should have at least one error
}
}
3. Authentication and Security Tests
SecurityTest.java
package com.example.restassured.tests;
import com.example.restassured.base.BaseApiTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Base64;
import java.util.Map;
import static org.hamcrest.Matchers.*;
class SecurityTest extends BaseApiTest {
@Test
@DisplayName("Should authenticate with valid credentials")
void shouldAuthenticateWithValidCredentials() {
String authToken = "valid-token"; // In real tests, get from login endpoint
given()
.spec(TestConfig.getRequestSpecWithAuth(authToken))
.when()
.get("/secure/users")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("$", hasKey("data"));
}
@Test
@DisplayName("Should return 401 for missing authentication")
void shouldReturnUnauthorizedForMissingAuthentication() {
given()
.spec(requestSpec)
.header("Authorization", "") // Empty authorization
.when()
.get("/secure/users")
.then()
.spec(TestConfig.getUnauthorizedResponseSpec())
.body("error", equalTo("Unauthorized"))
.body("message", containsString("authentication"));
}
@Test
@DisplayName("Should return 401 for invalid token")
void shouldReturnUnauthorizedForInvalidToken() {
given()
.spec(TestConfig.getRequestSpecWithAuth("invalid-token"))
.when()
.get("/secure/users")
.then()
.spec(TestConfig.getUnauthorizedResponseSpec());
}
@Test
@DisplayName("Should support Basic Authentication")
void shouldSupportBasicAuthentication() {
String username = "testuser";
String password = "testpass";
String authHeader = "Basic " + Base64.getEncoder()
.encodeToString((username + ":" + password).getBytes());
given()
.spec(requestSpec)
.header("Authorization", authHeader)
.when()
.get("/basic-auth")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("authenticated", equalTo(true));
}
@Test
@DisplayName("Should validate CORS headers")
void shouldValidateCorsHeaders() {
given()
.spec(requestSpec)
.header("Origin", "https://example.com")
.when()
.options("/users")
.then()
.statusCode(200)
.header("Access-Control-Allow-Origin", notNullValue())
.header("Access-Control-Allow-Methods", containsString("GET"))
.header("Access-Control-Allow-Headers", containsString("Content-Type"));
}
@Test
@DisplayName("Should prevent SQL injection in query parameters")
void shouldPreventSqlInjection() {
String sqlInjectionParam = "1; DROP TABLE users; --";
given()
.spec(requestSpec)
.queryParam("id", sqlInjectionParam)
.when()
.get("/users")
.then()
.spec(TestConfig.getBadRequestResponseSpec())
.body("error", not(emptyOrNullString()));
}
@Test
@DisplayName("Should validate content security policy")
void shouldValidateContentSecurityPolicy() {
given()
.spec(requestSpec)
.when()
.get("/users")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.header("Content-Security-Policy", not(emptyOrNullString()))
.header("X-Content-Type-Options", equalTo("nosniff"))
.header("X-Frame-Options", not(emptyOrNullString()));
}
@Test
@DisplayName("Should handle rate limiting")
void shouldHandleRateLimiting() {
// Make multiple rapid requests to trigger rate limiting
for (int i = 0; i < 10; i++) {
Response response = get("/users");
if (response.getStatusCode() == 429) {
response.then()
.body("error", equalTo("Too Many Requests"))
.body("message", containsString("rate limit"));
break;
}
}
}
}
4. File Upload and Download Tests
FileUploadDownloadTest.java
package com.example.restassured.tests;
import com.example.restassured.base.BaseApiTest;
import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import static org.hamcrest.Matchers.*;
class FileUploadDownloadTest extends BaseApiTest {
@Test
@DisplayName("Should upload file successfully")
void shouldUploadFileSuccessfully() throws IOException {
// Create a temporary file for testing
File file = File.createTempFile("test-upload", ".txt");
file.deleteOnExit();
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write("This is a test file content".getBytes());
}
given()
.spec(requestSpec)
.multiPart("file", file)
.formParam("description", "Test file upload")
.when()
.post("/upload")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("success", equalTo(true))
.body("filename", equalTo(file.getName()))
.body("size", greaterThan(0));
}
@Test
@DisplayName("Should upload multiple files")
void shouldUploadMultipleFiles() throws IOException {
File file1 = File.createTempFile("test1", ".txt");
File file2 = File.createTempFile("test2", ".txt");
file1.deleteOnExit();
file2.deleteOnExit();
try (FileOutputStream fos1 = new FileOutputStream(file1);
FileOutputStream fos2 = new FileOutputStream(file2)) {
fos1.write("File 1 content".getBytes());
fos2.write("File 2 content".getBytes());
}
given()
.spec(requestSpec)
.multiPart("files", file1)
.multiPart("files", file2)
.formParam("uploadType", "multiple")
.when()
.post("/upload/multiple")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.body("files", hasSize(2))
.body("files[0].filename", equalTo(file1.getName()))
.body("files[1].filename", equalTo(file2.getName()));
}
@Test
@DisplayName("Should download file successfully")
void shouldDownloadFileSuccessfully() throws IOException {
Response response = given()
.spec(requestSpec)
.when()
.get("/download/file.txt")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.contentType(not(emptyOrNullString()))
.extract()
.response();
// Save downloaded file
File downloadedFile = File.createTempFile("downloaded", ".txt");
downloadedFile.deleteOnExit();
try (FileOutputStream fos = new FileOutputStream(downloadedFile)) {
fos.write(response.asByteArray());
}
assertThat(downloadedFile.length()).isGreaterThan(0);
}
@Test
@DisplayName("Should validate file download headers")
void shouldValidateFileDownloadHeaders() {
given()
.spec(requestSpec)
.when()
.get("/download/file.txt")
.then()
.spec(TestConfig.getSuccessResponseSpec())
.header("Content-Type", not(emptyOrNullString()))
.header("Content-Disposition", containsString("attachment"))
.header("Content-Length", not(emptyOrNullString()));
}
@Test
@DisplayName("Should reject invalid file types")
void shouldRejectInvalidFileTypes() throws IOException {
File invalidFile = File.createTempFile("test", ".exe");
invalidFile.deleteOnExit();
try (FileOutputStream fos = new FileOutputStream(invalidFile)) {
fos.write("Malicious content".getBytes());
}
given()
.spec(requestSpec)
.multiPart("file", invalidFile)
.when()
.post("/upload")
.then()
.spec(TestConfig.getBadRequestResponseSpec())
.body("error", equalTo("Invalid file type"));
}
}
Step 6: Custom Matchers and Utilities
Custom REST Assured Matchers
CustomMatchers.java
package com.example.restassured.matchers;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class CustomMatchers {
private CustomMatchers() {
// Utility class
}
public static TypeSafeMatcher<String> isValidEmail() {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
@Override
public void describeTo(Description description) {
description.appendText("a valid email address");
}
@Override
protected void describeMismatchSafely(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ").appendValue(item);
}
};
}
public static TypeSafeMatcher<String> isValidPhoneNumber() {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String phone) {
return phone != null && phone.matches("^[\\d\\-\\(\\)\\s\\+]+$");
}
@Override
public void describeTo(Description description) {
description.appendText("a valid phone number");
}
@Override
protected void describeMismatchSafely(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ").appendValue(item);
}
};
}
public static TypeSafeMatcher<String> isIsoDateTime() {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String dateTime) {
if (dateTime == null) return false;
try {
LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_DATE_TIME);
return true;
} catch (DateTimeParseException e) {
return false;
}
}
@Override
public void describeTo(Description description) {
description.appendText("a valid ISO date time string");
}
@Override
protected void describeMismatchSafely(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ").appendValue(item);
}
};
}
public static TypeSafeMatcher<String> isFutureDateTime() {
return new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String dateTime) {
if (dateTime == null) return false;
try {
LocalDateTime parsed = LocalDateTime.parse(dateTime, DateTimeFormatter.ISO_DATE_TIME);
return parsed.isAfter(LocalDateTime.now());
} catch (DateTimeParseException e) {
return false;
}
}
@Override
public void describeTo(Description description) {
description.appendText("a future date time");
}
@Override
protected void describeMismatchSafely(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ").appendValue(item);
}
};
}
}
Response Validator Utility
ResponseValidator.java
package com.example.restassured.utils;
import io.restassured.response.Response;
import io.restassured.response.ValidatableResponse;
import java.util.function.Consumer;
import static org.hamcrest.Matchers.*;
public class ResponseValidator {
private ResponseValidator() {
// Utility class
}
public static Consumer<ValidatableResponse> successResponse() {
return response -> response
.statusCode(200)
.contentType(containsString("application/json"))
.body("success", equalTo(true));
}
public static Consumer<ValidatableResponse> createdResponse() {
return response -> response
.statusCode(201)
.contentType(containsString("application/json"))
.body("id", notNullValue());
}
public static Consumer<ValidatableResponse> badRequestResponse() {
return response -> response
.statusCode(400)
.body("success", equalTo(false))
.body("errors", not(empty()));
}
public static Consumer<ValidatableResponse> notFoundResponse() {
return response -> response
.statusCode(404)
.body("error", not(emptyOrNullString()));
}
public static Consumer<ValidatableResponse> unauthorizedResponse() {
return response -> response
.statusCode(401)
.body("error", equalTo("Unauthorized"));
}
public static void validatePagination(Response response, int expectedPage, int expectedSize) {
response.then()
.body("pagination.page", equalTo(expectedPage))
.body("pagination.size", equalTo(expectedSize))
.body("pagination.total", greaterThan(0))
.body("pagination.total_pages", greaterThan(0));
}
public static void validateUserResponse(Response response, String expectedName, String expectedEmail) {
response.then()
.body("id", notNullValue())
.body("name", equalTo(expectedName))
.body("email", equalTo(expectedEmail))
.body("created_at", not(emptyOrNullString()))
.body("updated_at", not(emptyOrNullString()));
}
}
Step 7: JSON Schema Validation
JSON Schema Files
src/test/resources/schemas/user-schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 1
},
"email": {
"type": "string",
"format": "email"
},
"phone": {
"type": "string"
},
"website": {
"type": "string",
"format": "uri"
},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"suite": {"type": "string"},
"city": {"type": "string"},
"zipcode": {"type": "string"},
"geo": {
"type": "object",
"properties": {
"lat": {"type": "string"},
"lng": {"type": "string"}
},
"required": ["lat", "lng"]
}
},
"required": ["street", "city", "zipcode"]
},
"company": {
"type": "object",
"properties": {
"name": {"type": "string"},
"catchPhrase": {"type": "string"},
"bs": {"type": "string"}
}
}
},
"required": ["id", "name", "email"]
}
Step 8: Test Execution and Reporting
Test Suite Configuration
TestSuites.java
package com.example.restassured.suites;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("REST API Test Suite")
@SelectPackages("com.example.restassured.tests")
public class TestSuites {
// This class serves as a test suite container
}
Allure Configuration
allure.properties
allure.results.directory=target/allure-results
allure.link.issue.pattern=https://example.com/issue/{}
allure.link.tms.pattern=https://example.com/tms/{}
allure.report.remove.results.after.build=true
Best Practices
1. Test Design
- Use Given/When/Then structure for readability
- Keep tests independent and isolated
- Use meaningful test names with @DisplayName
- Parameterize tests for data-driven testing
2. Maintenance
- Use Page Object pattern for API endpoints
- Centralize configuration and test data
- Implement custom matchers for complex validations
- Use JSON schema for response validation
3. Performance
- Set appropriate timeouts
- Use connection pooling
- Implement parallel test execution
- Monitor test execution time
4. Reporting
- Use Allure for comprehensive reporting
- Add attachments for requests and responses
- Implement custom steps for complex validations
- Track test execution history
Conclusion
This comprehensive REST Assured implementation provides:
- Fluent API Usage: Clean, readable test syntax
- Comprehensive Validation: Status codes, headers, response body, schemas
- Advanced Features: File upload/download, authentication, security testing
- Custom Utilities: Matchers, validators, test data factories
- Production Ready: Configuration management, error handling, reporting
Key Benefits:
- Readability: Fluent API makes tests easy to read and maintain
- Comprehensive Validation: Multiple validation strategies
- Extensibility: Custom matchers and utilities
- Professional Reporting: Allure integration for detailed reports
- Best Practices: Follows testing best practices and patterns
By implementing this solution, you can build robust, maintainable, and comprehensive API test suites that provide confidence in your API's functionality and reliability.