View all services
Talk to QA Advisor
/Blog/REST Assured for API Testing: A Practical Guide for QA Teams
Automation Testing6 min read

REST Assured for API Testing: A Practical Guide for QA Teams

A working REST Assured suite from first test to CI: request specifications, authentication, POJO serialisation, the GPath trap that makes a green test assert nothing, and where the library is the wrong tool.

Published September 15, 2026Last updated September 15, 2026
On this page

Most Java API suites start the same way. Someone writes a test with HttpClient, adds a JSON parser, writes a helper to attach the auth header, and six months later there are four helper classes nobody wants to touch. The tests work. Reading them tells you nothing about what the API is supposed to do.

REST Assured exists to remove that layer. It is a Java library that gives you a readable syntax for HTTP calls and response assertions, so the test reads as the contract rather than the plumbing. This assumes you already know what API testing covers and why it sits below the UI layer.

This guide builds a working suite against a real API, then covers the failures that cost teams the most time.

The short version
REST Assured gives Java teams readable API tests and assertion failures that name the path, the expected value and the actual one. Use request specifications from the start, set a content type on every POST, and make each new test fail once on purpose: a wrong GPath returns null rather than erroring, so a green test can be asserting nothing.

What you will build

By the end you will have a REST Assured suite that authenticates, validates response bodies, reuses request configuration across tests, and fails with a message that tells you what broke. Every snippet below runs.

Prerequisites

REST Assured is a library, not a framework. It needs a JDK, a build tool and a test runner. Version pinning matters here because the 4.x and 5.x lines differ in package names for several modules.

Version pinning matters: 4.x and 5.x differ in module packaging
ComponentVersion used hereNote
JDK17 or laterRecords used in the serialisation example need 16+
REST Assured5.5.04.x uses different package names for several modules
Hamcrest2.2Supplies the matchers used in every assertion
JUnit5.10+BeforeAll for one-time authentication
Jackson or Gsonon classpathRequired for automatic POJO serialisation

Add the dependency and the JSON matcher module. The json-path artefact ships with the core dependency, so you do not need it separately.

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest</artifactId>
    <version>2.2</version>
    <scope>test</scope>
</dependency>

Import the static methods. Without these two imports nothing in this guide compiles, and the error you get is a confusing "cannot find symbol" on given.

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

Step 1: The first test

REST Assured uses a given-when-then chain. given() configures the request, when() fires it, then() asserts on the response.

@Test
void returnsSingleUser() {
    given()
        .baseUri("https://reqres.in")
        .basePath("/api")
    .when()
        .get("/users/2")
    .then()
        .statusCode(200)
        .body("data.id", equalTo(2))
        .body("data.email", containsString("@"));
}

Run it and the test passes silently. That silence is the problem with a first test: you cannot tell whether it asserted anything. Break it deliberately by changing equalTo(2) to equalTo(99) and read the failure:

java.lang.AssertionError: 1 expectation failed.
JSON path data.id doesn't match.
Expected: <99>
  Actual: <2>

That message is the reason to use the library. It names the path, the expectation and the actual value without you writing any of it.

Step 2: Asserting on response bodies

The body() method takes a GPath expression, not a JsonPath expression. This trips up almost everyone arriving from a JavaScript or Python background, because the syntax looks similar and then behaves differently on collections.

For a response shaped like this:

{
  "page": 2,
  "data": [
    { "id": 7, "email": "[email protected]", "first_name": "Michael" },
    { "id": 8, "email": "[email protected]", "first_name": "Lindsay" }
  ]
}

These assertions work:

.body("data.size()", equalTo(2))
.body("data[0].first_name", equalTo("Michael"))
.body("data.first_name", hasItems("Michael", "Lindsay"))
.body("data.findAll { it.id > 7 }.first_name", hasItem("Lindsay"))

The last line is Groovy running inside the assertion. It is genuinely useful for filtering, and it is also where unreadable one-liners come from. Keep the filter simple or extract the response and assert in Java.

Response response = given().when().get("/users?page=2").then().extract().response();
List<String> names = response.jsonPath().getList("data.first_name");
assertThat(names).contains("Michael");

Extracting is the right choice whenever you need more than two assertions on the same payload. It also gives you a real debugger breakpoint.

Step 3: Removing duplication with specifications

Copying baseUri and auth headers into forty tests is how suites rot. REST Assured has request and response specifications for exactly this.

public class ApiSpec {
    public static RequestSpecification request() {
        return new RequestSpecBuilder()
            .setBaseUri("https://reqres.in")
            .setBasePath("/api")
            .setContentType(ContentType.JSON)
            .addHeader("x-api-key", System.getenv("API_KEY"))
            .build();
    }

    public static ResponseSpecification ok() {
        return new ResponseSpecBuilder()
            .expectStatusCode(200)
            .expectContentType(ContentType.JSON)
            .expectResponseTime(lessThan(3000L))
            .build();
    }
}

Tests then carry only what makes them different:

@Test
void listsUsers() {
    given().spec(ApiSpec.request())
    .when().get("/users?page=2")
    .then().spec(ApiSpec.ok())
        .body("data", hasSize(6));
}

Note the API key comes from the environment, never from the source. A credential in a test file reaches the repository history and stays there.

Specifications remove per-test duplication
ConcernWithout specificationsWith specifications
Base URIRepeated in every testSet once in RequestSpecBuilder
Auth headerCopied per test, easy to missApplied to every request that uses the spec
Content typeForgotten, causing empty POST bodiesSet centrally
Status and timing checksWritten per testAsserted by a shared ResponseSpecification
Changing the environmentFind and replace across filesOne builder edit

Step 4: Authentication

REST Assured has built-in support for the common schemes. Bearer tokens are the usual case:

given().auth().oauth2(token)

For basic auth, prefer preemptive(). The non-preemptive form waits for a 401 challenge before sending credentials, which fails silently against APIs that return 403 instead.

given().auth().preemptive().basic(username, password)

Fetch the token once per run rather than per test. A login call in a @BeforeAll keeps a fifty-test suite from making fifty extra round trips. Auth handling is also where API security testing overlaps with functional work: a suite that only ever tests the happy path never proves that an expired token is rejected.

@BeforeAll
static void authenticate() {
    token = given()
        .contentType(ContentType.JSON)
        .body(Map.of("email", System.getenv("USER"), "password", System.getenv("PASS")))
    .when()
        .post("https://reqres.in/api/login")
    .then()
        .statusCode(200)
        .extract().path("token");
}

Step 5: Serialisation with POJOs

String payloads are fine for two fields and unmanageable for twenty. REST Assured serialises objects automatically when Jackson or Gson is on the classpath.

record CreateUser(String name, String job) {}

@Test
void createsUser() {
    CreateUser payload = new CreateUser("morpheus", "leader");

    given().spec(ApiSpec.request())
        .body(payload)
    .when()
        .post("/users")
    .then()
        .statusCode(201)
        .body("name", equalTo("morpheus"));
}

If serialisation produces an empty body, the content type is almost always missing. REST Assured picks the serialiser from the content type, and with none set it does not know whether you want JSON or XML.

The errors that cost the most time

These are the failures that generate support questions, in rough order of how often they appear.

The failure that hides: a wrong GPath returns null instead of erroring
SymptomCauseFix
cannot find symbol: givenStatic imports missingimport static io.restassured.RestAssured.*
Assertion passes when it should failbody() path matches nothing and returns nullPrint with .log().body() and verify the path
IllegalStateException on a filterGPath closure applied to a non-collectionConfirm the node is an array before findAll
Empty request body on POSTNo content type setAdd .contentType(ContentType.JSON)
SSLHandshakeException internallySelf-signed certificaterelaxedHTTPSValidation, non-production only
401 despite correct credentialsNon-preemptive basic authUse .auth().preemptive().basic(...)
Passes locally, fails in CIEnv vars absent in the runnerFail fast on a null credential in BeforeAll

The second row is the dangerous one. A wrong GPath does not error, it returns null, and body("data.nam", equalTo(null)) is a green test asserting nothing. When a test has never failed, make it fail once on purpose.

Logging, and the one setting worth changing

By default a failing test prints the assertion but not the payload. This turns most debugging into a re-run with logging bolted on. Set it globally instead:

RestAssured.filters(new RequestLoggingFilter(LogDetail.URI),
                    new ResponseLoggingFilter(LogDetail.ALL));

Or, less noisily, log only on failure:

.then().log().ifValidationFails(LogDetail.BODY)

The second form is the better default for CI, where full logging on a large suite produces output nobody reads.

🔬 From our work
We have no published QAble case study on a REST Assured migration, so we are not putting a number on this. The pattern our API engagements do show consistently is that suites arrive without request specifications, and the retrofit is the expensive part: configuration is duplicated across every test file, so changing an environment or an auth scheme means editing all of them. That observation is qualitative and unquantified. Adding specifications before roughly the twentieth test is the cheapest point to do it.

Where REST Assured is the wrong tool

Naming the limits is more useful than another feature tour.

It is a Java library, so a team with no Java skills will fight it. If your service code is TypeScript and your engineers are not JVM developers, Supertest or Playwright's request context will cost less to maintain, and maintenance dominates the total cost of an API suite.

It tests one request at a time. Contract testing between services is a different problem, and Pact solves it properly. Using REST Assured to assert both sides of a contract couples your suite to two deployments at once.

It is not a load tool. The expectResponseTime matcher asserts a single call's latency and tells you nothing about behaviour under concurrency. That is JMeter or Gatling territory.

For exploratory work against an unfamiliar API, Postman is faster. Write the exploration in Postman, then port the cases worth keeping into REST Assured where they can run in CI.

Where to start

Pick one endpoint your team breaks most often. Write three tests: the success path, one validation failure, and one authorisation failure. Put them in CI on the pull request.

Three tests running on every merge beat forty tests running when someone remembers. Once they hold, add the specification classes from step 3 before the suite reaches twenty tests, because retrofitting them later means touching every file. If the suite needs to run on every commit rather than nightly, the Postman, Newman and CI/CD pattern covers the pipeline side in more depth.

If you want a second pair of eyes on an API suite that has grown past the point of comfort, our API testing services team reviews structure, coverage and CI integration.

Frequently Asked Questions

Is REST Assured still maintained?

Yes. The 5.x line is current and receives releases. The 4.x line is not recommended for new projects because several modules changed package names between the two, and most current documentation and examples target 5.x.

Do I need a separate JSON path library?

No. The `json-path` module ships with the core `rest-assured` dependency. Add Hamcrest for matchers, and Jackson or Gson if you want automatic object serialisation.

Why does my assertion pass when the API is returning the wrong data?

Almost always a wrong GPath expression. An invalid path returns null rather than raising an error, so the assertion compares null against null and passes. Print the body with `.log().body()` and confirm the path exists, then make the test fail once deliberately.

Should I use REST Assured or Postman?

They solve different problems. Postman is faster for exploring an unfamiliar API and for sharing a collection with people who do not write Java. REST Assured belongs in CI, in the same repository and language as the service, where tests run on every pull request. Many teams use both: explore in Postman, port the cases worth keeping.

Can REST Assured handle GraphQL?

It can send the request, since GraphQL is usually a POST with a JSON body, and you can assert on the response normally. What it does not give you is schema awareness or query validation, so for a GraphQL-heavy service a purpose-built client is usually less work.

How do I stop credentials appearing in the repository?

Read them from environment variables and fail fast when they are missing. A credential committed to a test file remains in the repository history after deletion, so treat any committed secret as compromised and rotate it.

Is REST Assured suitable for performance testing?

No. The `expectResponseTime` matcher asserts the latency of one call and says nothing about behaviour under concurrent load. Use JMeter or Gatling for that, and keep the two concerns in separate suites.

How many API tests should run on a pull request?

Fewer than you think, and reliably. Three tests covering the success path, a validation failure and an authorisation failure on your most frequently broken endpoint deliver more value than a large suite that only runs when someone remembers to trigger it.

Free Assessment

Get a free QA audit for your project

Identify quality gaps before they become production bugs.

Get Free Audit

Ship software with confidence

Talk to a QA advisor and find out how QAble can help your team build quality in at every stage.

No sales pitch
Technical walkthrough
No lock-in commitment

Talk to QA Advisor

Direct access to QAble's QA specialists.

Response within 24 hours