DEV Community

MARIELA ESTEFANY RAMOS LOZA
MARIELA ESTEFANY RAMOS LOZA

Posted on

Applying API Testing Frameworks: A Real-World Guide with REST Assured

Introduction

In modern software development, APIs (Application Programming Interfaces) are the backbone of virtually every application. Whether you are building a mobile app, a microservice architecture, or a web platform, the reliability of your APIs directly determines the quality of your product.

But how do we ensure our APIs actually work as expected — before users find out they don't?

That's where API Testing Frameworks come in. According to the widely referenced article "Top 10 API Testing Tools in 2020" published on Medium, the industry has converged on a set of powerful tools to automate this process. Among them, REST Assured consistently ranks as one of the top choices for Java-based teams.

In this article, you will learn:

  • What REST Assured is and why it matters
  • How to set it up from scratch
  • How to write real-world API tests against a live REST API
  • Best practices to make your tests maintainable and scalable

What Is REST Assured?

REST Assured is an open-source Java library that simplifies testing and validating REST APIs. Created by Johan Haleby and now maintained by the community, it provides a domain-specific language (DSL) that reads almost like plain English, making tests both powerful and easy to understand.

Why REST Assured?

Feature Benefit
BDD-style syntax (given/when/then) Tests are readable by non-developers
Native JSON & XML parsing No manual deserialization needed
Full HTTP support (GET, POST, PUT, DELETE, PATCH) Covers all REST operations
Seamless JUnit/TestNG integration Works inside your existing CI/CD pipeline
Schema validation Validate JSON structure automatically
OAuth2 / JWT support Ready for secured APIs

Project Setup

Prerequisites

  • Java 11+
  • Maven or Gradle
  • An IDE (IntelliJ IDEA or Eclipse)

Maven Dependencies (pom.xml)

<dependencies>
    <!-- REST Assured core -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>

    <!-- JSON Schema Validator -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>json-schema-validator</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>

    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.0</version>
        <scope>test</scope>
    </dependency>

    <!-- Hamcrest matchers -->
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest</artifactId>
        <version>2.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

Project Structure

src/
└── test/
    └── java/
        └── com/
            └── apitesting/
                ├── config/
                │   └── BaseTest.java       ← shared configuration
                ├── tests/
                │   ├── GetUsersTest.java
                │   ├── CreateUserTest.java
                │   ├── UpdateUserTest.java
                │   └── DeleteUserTest.java
                └── models/
                    └── User.java           ← POJO for deserialization
Enter fullscreen mode Exit fullscreen mode

The Target API

For all examples in this article, we will test against Reqres — a free, public REST API specifically designed for testing and prototyping. It supports GET, POST, PUT, PATCH, and DELETE operations without requiring authentication.

Base URL: https://reqres.in/api


Real-World Code Examples

1. Base Configuration (BaseTest.java)

Setting up a centralized configuration avoids repeating the base URL and common headers across every test file.

package com.apitesting.config;

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import org.junit.jupiter.api.BeforeAll;

import static org.hamcrest.Matchers.lessThan;

public class BaseTest {

    protected static RequestSpecification requestSpec;
    protected static ResponseSpecification responseSpec;

    @BeforeAll
    public static void setup() {
        // Base request specification — shared by all tests
        requestSpec = new RequestSpecBuilder()
            .setBaseUri("https://reqres.in/api")
            .setContentType(ContentType.JSON)
            .addHeader("Accept", "application/json")
            .build();

        // Base response specification — all responses should reply under 3 seconds
        responseSpec = new ResponseSpecBuilder()
            .expectResponseTime(lessThan(3000L))
            .build();

        // Enable request/response logging for failed tests
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
    }
}
Enter fullscreen mode Exit fullscreen mode

2. GET Request — Retrieve a List of Users

This test validates that the API returns a paginated list of users with the correct structure.

package com.apitesting.tests;

import com.apitesting.config.BaseTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

public class GetUsersTest extends BaseTest {

    @Test
    @DisplayName("GET /users - should return paginated list with correct structure")
    public void shouldReturnPaginatedUsersList() {
        given()
            .spec(requestSpec)
            .queryParam("page", 2)
        .when()
            .get("/users")
        .then()
            .spec(responseSpec)
            .statusCode(200)
            .body("page", equalTo(2))
            .body("per_page", equalTo(6))
            .body("data", hasSize(6))
            .body("data[0].id", equalTo(7))
            .body("data[0].email", containsString("@reqres.in"))
            .body("data.first_name", everyItem(notNullValue()));
    }

    @Test
    @DisplayName("GET /users/{id} - should return single user by ID")
    public void shouldReturnSingleUserById() {
        int userId = 2;

        given()
            .spec(requestSpec)
        .when()
            .get("/users/{id}", userId)
        .then()
            .spec(responseSpec)
            .statusCode(200)
            .body("data.id", equalTo(userId))
            .body("data.email", equalTo("janet.weaver@reqres.in"))
            .body("data.first_name", equalTo("Janet"))
            .body("data.last_name", equalTo("Weaver"));
    }

    @Test
    @DisplayName("GET /users/{id} - should return 404 for non-existent user")
    public void shouldReturn404ForNonExistentUser() {
        given()
            .spec(requestSpec)
        .when()
            .get("/users/{id}", 9999)
        .then()
            .statusCode(404)
            .body(equalTo("{}"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Running this test produces output like:

✅ GET /users - should return paginated list with correct structure PASSED (342ms)
✅ GET /users/{id} - should return single user by ID PASSED (198ms)
✅ GET /users/{id} - should return 404 for non-existent user PASSED (201ms)
Enter fullscreen mode Exit fullscreen mode

3. POST Request — Create a New User

Testing resource creation requires validating the response body, status code (201), and that the server assigns an ID.

package com.apitesting.tests;

import com.apitesting.config.BaseTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

public class CreateUserTest extends BaseTest {

    @Test
    @DisplayName("POST /users - should create a new user and return 201")
    public void shouldCreateNewUser() {
        String requestBody = """
            {
                "name": "Mariela García",
                "job": "QA Engineer"
            }
            """;

        given()
            .spec(requestSpec)
            .body(requestBody)
        .when()
            .post("/users")
        .then()
            .spec(responseSpec)
            .statusCode(201)
            .body("name", equalTo("Mariela García"))
            .body("job", equalTo("QA Engineer"))
            .body("id", notNullValue())           // server assigns an ID
            .body("createdAt", notNullValue());    // server sets a timestamp
    }

    @Test
    @DisplayName("POST /users - should return id as a string")
    public void shouldReturnIdAsString() {
        String requestBody = """
            {
                "name": "Test User",
                "job": "Developer"
            }
            """;

        String createdId =
            given()
                .spec(requestSpec)
                .body(requestBody)
            .when()
                .post("/users")
            .then()
                .statusCode(201)
                .extract()
                .path("id");  // Extract the generated ID for chaining

        System.out.println("✅ User created with ID: " + createdId);
        // Can now use this ID in a follow-up GET request
    }
}
Enter fullscreen mode Exit fullscreen mode

4. PUT Request — Full Update of a Resource

package com.apitesting.tests;

import com.apitesting.config.BaseTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

public class UpdateUserTest extends BaseTest {

    @Test
    @DisplayName("PUT /users/{id} - should fully update user and return 200")
    public void shouldFullyUpdateUser() {
        int userId = 2;
        String updateBody = """
            {
                "name": "Mariela Updated",
                "job": "Senior QA Engineer"
            }
            """;

        given()
            .spec(requestSpec)
            .body(updateBody)
        .when()
            .put("/users/{id}", userId)
        .then()
            .spec(responseSpec)
            .statusCode(200)
            .body("name", equalTo("Mariela Updated"))
            .body("job", equalTo("Senior QA Engineer"))
            .body("updatedAt", notNullValue());
    }

    @Test
    @DisplayName("PATCH /users/{id} - should partially update user")
    public void shouldPartiallyUpdateUser() {
        int userId = 2;
        String patchBody = """
            {
                "job": "Lead QA Engineer"
            }
            """;

        given()
            .spec(requestSpec)
            .body(patchBody)
        .when()
            .patch("/users/{id}", userId)
        .then()
            .spec(responseSpec)
            .statusCode(200)
            .body("job", equalTo("Lead QA Engineer"))
            .body("updatedAt", notNullValue());
    }
}
Enter fullscreen mode Exit fullscreen mode

5. DELETE Request — Remove a Resource

package com.apitesting.tests;

import com.apitesting.config.BaseTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;

public class DeleteUserTest extends BaseTest {

    @Test
    @DisplayName("DELETE /users/{id} - should return 204 No Content")
    public void shouldDeleteUser() {
        int userId = 2;

        given()
            .spec(requestSpec)
        .when()
            .delete("/users/{id}", userId)
        .then()
            .statusCode(204);
        // 204 means success with no response body — exactly what REST expects
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Advanced: JSON Schema Validation

One of REST Assured's most powerful features is JSON Schema validation — ensuring the API response matches a predefined structure contract.

Schema file (src/test/resources/schemas/users_schema.json):

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["page", "per_page", "total", "total_pages", "data"],
  "properties": {
    "page":        { "type": "integer" },
    "per_page":    { "type": "integer" },
    "total":       { "type": "integer" },
    "total_pages": { "type": "integer" },
    "data": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "email", "first_name", "last_name", "avatar"],
        "properties": {
          "id":         { "type": "integer" },
          "email":      { "type": "string", "format": "email" },
          "first_name": { "type": "string" },
          "last_name":  { "type": "string" },
          "avatar":     { "type": "string", "format": "uri" }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Test using the schema:

import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

@Test
@DisplayName("GET /users - response must match JSON schema contract")
public void shouldMatchJsonSchema() {
    given()
        .spec(requestSpec)
        .queryParam("page", 1)
    .when()
        .get("/users")
    .then()
        .statusCode(200)
        .body(matchesJsonSchemaInClasspath("schemas/users_schema.json"));
}
Enter fullscreen mode Exit fullscreen mode

This kind of test is invaluable in a microservices environment — it catches breaking changes in the API contract before they reach production.


7. Authentication — Testing a Secured Endpoint

Real APIs require authentication. REST Assured handles this elegantly:

@Test
@DisplayName("POST /login - should authenticate and return token")
public void shouldAuthenticateAndReturnToken() {
    String credentials = """
        {
            "email": "eve.holt@reqres.in",
            "password": "cityslicka"
        }
        """;

    // Step 1: Authenticate and extract token
    String token =
        given()
            .spec(requestSpec)
            .body(credentials)
        .when()
            .post("/login")
        .then()
            .statusCode(200)
            .body("token", notNullValue())
            .extract()
            .path("token");

    System.out.println("Token received: " + token);

    // Step 2: Use token in a subsequent request
    given()
        .spec(requestSpec)
        .header("Authorization", "Bearer " + token)
    .when()
        .get("/users")
    .then()
        .statusCode(200);
}

@Test
@DisplayName("POST /login - should return 400 for missing password")
public void shouldReturn400ForMissingPassword() {
    String badCredentials = """
        {
            "email": "eve.holt@reqres.in"
        }
        """;

    given()
        .spec(requestSpec)
        .body(badCredentials)
    .when()
        .post("/login")
    .then()
        .statusCode(400)
        .body("error", equalTo("Missing password"));
}
Enter fullscreen mode Exit fullscreen mode

Running the Tests

From the command line (Maven)

# Run all tests
mvn test

# Run a specific test class
mvn test -Dtest=GetUsersTest

# Run with detailed output
mvn test -Dtest=GetUsersTest -pl . --also-make
Enter fullscreen mode Exit fullscreen mode

Expected Console Output

[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0

✅ shouldReturnPaginatedUsersList        PASSED (342ms)
✅ shouldReturnSingleUserById            PASSED (198ms)
✅ shouldReturn404ForNonExistentUser     PASSED (201ms)
✅ shouldCreateNewUser                   PASSED (415ms)
✅ shouldReturnIdAsString                PASSED (380ms)
✅ shouldFullyUpdateUser                 PASSED (265ms)
✅ shouldDeleteUser                      PASSED (188ms)
✅ shouldMatchJsonSchema                 PASSED (320ms)

[INFO] BUILD SUCCESS
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Use Specification Builders

Never hardcode base URLs or headers. Use RequestSpecBuilder and ResponseSpecBuilder to share configuration across all tests (as shown in BaseTest.java).

2. Separate Test Data from Test Logic

Use POJOs or external JSON files for request/response bodies — never inline hardcoded strings in complex scenarios.

// ❌ Avoid
.body("{ \"name\": \"Mariela\", \"job\": \"QA\" }")

// ✅ Prefer — use a POJO
User user = new User("Mariela", "QA Engineer");
given().body(user).when().post("/users")...
Enter fullscreen mode Exit fullscreen mode

3. Validate Both Happy Path and Error Scenarios

Always test what happens when things go wrong — missing fields, invalid IDs, unauthorized requests. Error cases reveal more bugs than success cases.

4. Assert Response Time

Slow APIs degrade user experience. Always include a response time assertion in your base spec:

.expectResponseTime(lessThan(2000L)) // Fail if response > 2 seconds
Enter fullscreen mode Exit fullscreen mode

5. Integrate into CI/CD

REST Assured tests are just JUnit tests — add them to your GitHub Actions or Jenkins pipeline:

# .github/workflows/api-tests.yml
- name: Run API Tests
  run: mvn test
Enter fullscreen mode Exit fullscreen mode

Conclusion

REST Assured is one of the most mature and expressive API testing frameworks available today. Its BDD-style syntax makes tests readable for the entire team, while its deep Java integration means it fits naturally into enterprise development workflows.

In this article, we covered:

  • ✅ Project setup with Maven
  • ✅ Base configuration with shared specifications
  • ✅ Real-world tests: GET, POST, PUT, PATCH, DELETE
  • ✅ JSON Schema validation for contract testing
  • ✅ Authentication flows with token extraction
  • ✅ Best practices for maintainable test suites

By applying these patterns, your team can catch API regressions before they ever reach production — saving time, reducing bugs, and building confidence in every deployment.


References

  1. Moataz Eldebsy. Top 10 API Testing Tools in 2020 (Detailed Reviews). Medium. https://medium.com/@moatazeldebsy/top-10-api-testing-tools-in-2020-detailed-reviews-7b2e28e29c5a
  2. REST Assured Official Documentation. https://rest-assured.io/
  3. REST Assured GitHub Repository. https://github.com/rest-assured/rest-assured
  4. Reqres — Fake REST API for Testing. https://reqres.in
  5. JUnit 5 User Guide. https://junit.org/junit5/docs/current/user-guide/
  6. JSON Schema Specification. https://json-schema.org/

© 2026 Mariela — Software Quality Engineering

Top comments (0)