Resource library

QA How-To

REST Assured given when then: A Practical Guide (2026)

Learn REST Assured given when then with runnable Java examples for requests, assertions, extraction, specifications, auth, logging, and API test design.

21 min read | 2,937 words

TL;DR

REST Assured given when then is a readable Java DSL: `given()` builds the request, `when()` sends it, and `then()` validates the response. Keep each phase focused, reuse specifications for stable defaults, and assert the business contract rather than only the status code.

Key Takeaways

  • Use given for request setup, when for dispatch, and then for response validation.
  • Make request and response specifications immutable test-suite building blocks.
  • Validate status, headers, and meaningful body behavior instead of asserting only HTTP 200.
  • Extract values after core assertions and pass them explicitly into later requests.
  • Log on validation failure while masking secrets and avoiding noisy always-on logs.
  • Keep scenarios independent by creating their own data and cleaning it through supported APIs.

REST Assured given when then organizes an API test into request context, request execution, and response expectations. A concise flow such as given().queryParam(...).when().get(...).then().statusCode(200) is readable, but production-quality tests also need clear contracts, reusable specifications, safe logging, deterministic data, and assertions that explain failures.

This practical guide uses Java, JUnit 5, Hamcrest, and the REST Assured 6.x API. The DSL remains familiar to teams on supported 5.5.x builds, but new projects should select a version compatible with their Java and framework baseline.

TL;DR

import org.junit.jupiter.api.Test;

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

class HealthApiTest {
    @Test
    void reports_service_health() {
        given()
            .baseUri(System.getProperty("baseUrl", "http://localhost:8080"))
            .accept("application/json")
        .when()
            .get("/api/health")
        .then()
            .statusCode(200)
            .contentType("application/json")
            .body("status", equalTo("UP"));
    }
}
Phase Responsibility Typical methods
given() Define request context baseUri, header, queryParam, auth, body
when() Dispatch one HTTP request get, post, put, patch, delete, request
then() Validate the response statusCode, header, contentType, body, time
extract() Return validated data path, response, as

1. How REST Assured given when then Works

The three words are syntactic boundaries in a fluent Java DSL. given() returns a request specification that accumulates configuration. when() signals that request setup is complete. An HTTP verb sends the request and returns a response. then() converts that response into a validatable response on which status, headers, cookies, content type, and body can be asserted.

given()
    .queryParam("state", "active")
.when()
    .get("/api/users")
.then()
    .statusCode(200);

when() and then() do not implement separate network stages. The request is sent when the HTTP method executes. The words make the test read as behavior: given an active-state filter, when clients get users, then the service responds successfully. and() is optional syntactic sugar and does not change execution.

You can omit phases in compact expressions. get('/health').then() is legal after static import, and when().get(...) works without request setup. Keep explicit phases when they improve understanding. Do not add given() solely to imitate a template when there is no context.

The chain is mutable while it is being built, so avoid sharing one in-progress request between tests. Reusable RequestSpecification and ResponseSpecification objects are the correct abstraction for common defaults. Each test should still create its own chain and dispatch its own request.

Static imports make the DSL readable. Import io.restassured.RestAssured.* methods intentionally and Hamcrest matchers from org.hamcrest.Matchers.*. Avoid a mix of fully qualified calls and wildcard-heavy application imports that make method origins unclear.

2. Set Up REST Assured 6 with JUnit 5

A Maven project needs REST Assured and the JUnit Jupiter test engine. Version 6.0.0 is a current major line for the 2026 API examples. If your organization stays on the supported 5.5.x line for framework compatibility, the core DSL shown here remains the same.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>api-tests</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <junit.version>5.12.2</junit.version>
    <rest-assured.version>6.0.0</rest-assured.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <version>${rest-assured.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.3</version>
      </plugin>
    </plugins>
  </build>
</project>

Put tests under src/test/java and run them against the service URL supplied by CI.

mvn test -DbaseUrl=http://localhost:8080

Do not hard-code a shared staging URL, credentials, or tenant IDs inside the test. Read non-secret settings from system properties or environment-backed configuration, and inject secrets through the CI secret store. Fail early with a clear message when a required value is absent. The API testing fundamentals guide explains how environments, risk, and test layers fit together.

3. Build the given Phase Correctly

The given phase describes everything sent to the server: base URI, path parameters, query parameters, headers, cookies, authentication, content type, and body. Keep it declarative. Preparing unrelated database state or driving a browser inside the chain hides prerequisites and makes failure diagnosis harder.

record CreateUserRequest(String name, String email, String role) {}

CreateUserRequest payload = new CreateUserRequest(
    "Asha Verma",
    "asha@example.test",
    "reviewer"
);

given()
    .baseUri(baseUrl)
    .basePath("/api")
    .contentType("application/json")
    .accept("application/json")
    .header("X-Correlation-Id", "test-create-user-001")
    .body(payload)
.when()
    .post("/users")
.then()
    .statusCode(201);

REST Assured serializes the record when an object mapper is available on the classpath. In a Spring-based test project, Jackson is commonly already present. Otherwise add the mapper your suite standardizes on. You can pass a JSON string, but typed payload objects catch field mistakes during compilation and reduce quote-heavy fixtures.

Use the specific parameter method that communicates protocol intent. queryParam('page', 2) belongs after ?; pathParam('userId', id) replaces a path placeholder; formParam creates form data; multiPart creates multipart requests. The generic param method can infer behavior from the HTTP method, but explicit methods are easier to review.

given()
    .pathParam("userId", userId)
    .queryParam("include", "permissions")
.when()
    .get("/api/users/{userId}")
.then()
    .statusCode(200);

Do not log bearer tokens or session cookies. If diagnostic filters log headers, blacklist sensitive names through REST Assured log configuration or use a sanitized custom filter. Test logs often outlive the process and may be visible to a larger audience than the secret store.

4. Keep the when Phase Focused on One Request

The when phase should contain one dispatch operation. REST Assured offers named methods for common verbs and request for a verb that needs to be selected dynamically.

given()
    .baseUri(baseUrl)
    .pathParam("id", userId)
.when()
    .delete("/api/users/{id}")
.then()
    .statusCode(204)
    .body(org.hamcrest.Matchers.emptyOrNullString());

A single fluent chain represents a single HTTP exchange. If a scenario creates a resource, updates it, and retrieves it, use three named chains with extracted values between them. Do not make a helper that silently sends several requests while its name suggests one action. Clear boundaries make request logs and traces align with scenario steps.

The HTTP method has contract meaning. Use POST for the service's documented create or command operation, PUT for full replacement when the API defines it that way, PATCH for partial updates, and DELETE for deletion. A test should validate the API's actual semantics, not impose generic REST folklore on an intentionally different protocol.

Use redirects().follow(false) in given when redirect behavior itself is under test. Otherwise automatic following can cause the final 200 to hide an incorrect 302. Likewise, decide whether URL encoding should occur. REST Assured encodes parameters by default; disable it only when passing a pre-encoded value and add a focused assertion so double-encoding regressions are visible.

Time-sensitive behavior belongs around the request, not inside a fixed sleep. Poll a status endpoint with a bounded retry library or JUnit-compatible helper when the system is eventually consistent. One long Thread.sleep makes the suite slow and still races variable processing time.

5. Validate More Than the Status in then

The then phase is where an API test earns its value. Status code is necessary, but the same status can carry the wrong representation, missing headers, stale data, or a malformed error. Validate the parts of the public contract relevant to the scenario.

given()
    .baseUri(baseUrl)
    .pathParam("id", userId)
.when()
    .get("/api/users/{id}")
.then()
    .statusCode(200)
    .contentType("application/json")
    .header("Cache-Control", org.hamcrest.Matchers.containsString("no-store"))
    .body("id", equalTo(userId))
    .body("email", equalTo("asha@example.test"))
    .body("roles", hasItem("reviewer"));

Body paths use REST Assured's JSON path support, including Groovy collection expressions. Prefer simple paths and Hamcrest matchers for ordinary assertions. Extremely clever expressions can become unreadable and difficult to debug; deserialize into a domain response object when several related fields need Java-level logic.

Assert types accurately. JSON integer values commonly map to Java integer-like values, while decimals can be float or double depending on configuration. A matcher expecting 12.50 as a Java double may not match a parsed float. Configure a BIG_DECIMAL number return type for money-sensitive assertions or compare a deserialized decimal domain field.

Avoid asserting volatile fields exactly unless they are the subject. A generated UUID should be nonblank and structurally valid, not equal to a value nobody controls. A timestamp can be parsed and checked within the request window, rather than copied into a golden body. Selective assertions reduce noise without weakening the contract.

Use JSON schema validation as a complement, not a replacement for business assertions. A schema can verify required keys and types yet allow the wrong owner, total, or state.

6. Extract Values After Validation

Real API workflows pass identifiers, tokens, version values, or links from one request to another. Validate the create response first, then extract what the next step needs. This ordering prevents a malformed response from being treated as valid setup.

String userId =
    given()
        .baseUri(baseUrl)
        .contentType("application/json")
        .body(new CreateUserRequest("Asha Verma", "asha@example.test", "reviewer"))
    .when()
        .post("/api/users")
    .then()
        .statusCode(201)
        .body("id", not(blankOrNullString()))
        .body("email", equalTo("asha@example.test"))
        .extract()
        .path("id");

given()
    .baseUri(baseUrl)
    .pathParam("id", userId)
.when()
    .get("/api/users/{id}")
.then()
    .statusCode(200)
    .body("id", equalTo(userId));

Use .extract().response() when several values or metadata are needed, then query the Response. Use .extract().as(UserResponse.class) when the response maps cleanly to a type. Keep the original response available during diagnosis if deserialization errors could hide server output.

Do not store extracted values in static mutable fields shared across test classes. Parallel execution can overwrite them, and test order becomes an accidental dependency. Keep data inside the test, a scenario object, or a fixture with test-scoped lifetime. Create unique data for each test and clean it through a supported API when cleanup is safe and necessary.

Chained workflow tests are valuable, but do not make every read test create data through a long public workflow. A sanctioned fixture API or direct setup layer may be faster and more diagnostic. Separate setup mechanics from the behavior being tested while ensuring setup cannot bypass the very rule under examination.

7. Reuse Request and Response Specifications

Specifications remove stable repetition without hiding scenario-specific information. Build them once per suite or class from configuration, then combine them with each test's unique parameters and body.

import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;

import static io.restassured.http.ContentType.JSON;

RequestSpecification apiRequest(String baseUrl, String token) {
    return new RequestSpecBuilder()
        .setBaseUri(baseUrl)
        .setBasePath("/api")
        .setAccept(JSON)
        .setContentType(JSON)
        .addHeader("Authorization", "Bearer " + token)
        .build();
}

ResponseSpecification successfulJson() {
    return new ResponseSpecBuilder()
        .expectContentType(JSON)
        .build();
}
given()
    .spec(apiRequest(baseUrl, token))
    .queryParam("limit", 20)
.when()
    .get("/users")
.then()
    .spec(successfulJson())
    .statusCode(200)
    .body("items.size()", lessThanOrEqualTo(20));

Keep status codes out of an overly broad response spec if endpoints legitimately return 200, 201, 202, or 204. A generic successfulJson spec should not claim JSON for a 204 response either. Build small specs with truthful names, such as createdJson, validationProblem, or noContent.

Prefer builders to mutating global fields such as RestAssured.baseURI in suites that run tests in parallel or target multiple services. Global defaults are convenient for small projects but create cross-test coupling when one class changes them. If legacy tests use globals, reset them after the class and prevent concurrent mutation.

Specifications are configuration, not assertion dumping grounds. A test reviewer should see the scenario's critical expected status and domain fields near the request. Reuse protocol-wide expectations while keeping business meaning local.

8. Test Authentication, Headers, and Error Contracts

REST Assured supports basic, preemptive basic, digest, OAuth-related flows, certificates, and custom headers. Match the mechanism the service exposes. For bearer tokens, a header or an OAuth2 helper is typical.

given()
    .baseUri(baseUrl)
    .auth().oauth2(accessToken)
.when()
    .get("/api/me")
.then()
    .statusCode(200)
    .body("permissions", hasItem("profile:read"));

Authentication tests need negative cases. Verify missing credentials, malformed tokens, expired tokens supplied by a controlled fixture, insufficient scopes, and cross-tenant access. Distinguish 401, which generally means authentication is missing or invalid, from 403, which means the authenticated principal is not allowed under the service contract. Do not weaken assertions to accept either unless the documented security design explicitly permits both.

A consistent problem response makes failures easier for clients to handle. Test its media type, stable code, safe message, field errors, and correlation identifier. Never expect a stack trace, SQL text, internal class name, or secret in an error body.

given()
    .baseUri(baseUrl)
    .contentType("application/json")
    .body("{\"email\":\"not-an-email\"}")
.when()
    .post("/api/users")
.then()
    .statusCode(400)
    .contentType("application/problem+json")
    .body("code", equalTo("VALIDATION_FAILED"))
    .body("errors.email", hasItem("must be a valid email address"))
    .body("traceId", not(blankOrNullString()));

Security behavior deserves a structured matrix. The API security testing checklist covers authorization boundaries, data exposure, rate limiting, and input risks beyond the happy path.

9. Log Failures Without Leaking Secrets

REST Assured can log request and response details. Always-on .log().all() is useful while developing one test but becomes noisy in CI and can expose credentials. Prefer failure-triggered logging.

given()
    .baseUri(baseUrl)
    .log().ifValidationFails()
.when()
    .get("/api/users/{id}", userId)
.then()
    .log().ifValidationFails()
    .statusCode(200);

Centralize failure logging through LogConfig or filters so every test follows the same policy. Blacklist sensitive headers, and remember that bodies can contain passwords, refresh tokens, personal information, or payment data. Masking Authorization alone is not enough. Use synthetic test data and limit log retention.

A request log generated before dispatch may not include headers added later by the underlying HTTP client or a subsequent filter. Treat it as REST Assured specification diagnostics, not definitive wire capture. If an integration defect depends on exact wire bytes, use controlled server logging or an approved network diagnostic tool in a secure environment.

Add a correlation ID to each request and include it in assertion messages or reports. This lets service owners join a failed client test to server traces without publishing the entire payload. Generate a unique ID per test or request and keep it in local scenario context.

Do not catch AssertionError merely to print the body and then forget to rethrow it. That converts a genuine failure into a passing test. REST Assured's conditional logging or a JUnit extension can attach diagnostics while preserving the result. For suite-level design and observability patterns, see REST API automation best practices.

10. Design Data-Driven and Negative Tests

JUnit parameterized tests are a good fit for validation boundaries. Supply the input, expected status, and stable error code as arguments. Keep each case independently readable in the report.

@org.junit.jupiter.params.ParameterizedTest(name = "email {0} returns {1}")
@org.junit.jupiter.params.provider.CsvSource(value = {
    "missing-at-sign,400,INVALID_EMAIL",
    "' ',400,EMAIL_REQUIRED",
    "a@b.test,201,NONE"
})
void validates_email(String email, int status, String code) {
    var response = given()
        .baseUri(baseUrl)
        .contentType("application/json")
        .body(new CreateUserRequest("API Test", email, "reviewer"))
    .when()
        .post("/api/users")
    .then()
        .statusCode(status);

    if (status >= 400) {
        response.body("code", equalTo(code));
    }
}

Add the junit-jupiter-params dependency if your dependency management does not already include it. For complex objects, use @MethodSource instead of putting JSON into CSV strings. Keep the number of combinations risk-based. Pairwise or boundary-focused cases are usually more informative than a giant Cartesian product that repeats the same behavior.

Negative tests should target meaningful equivalence classes: absent required fields, null, wrong types, boundary lengths, invalid enum values, duplicate idempotency keys, unauthorized ownership, malformed content types, and concurrent updates. Validate the full error contract and confirm no forbidden side effect occurred.

Make generated test data reproducible. Log a random seed or use deterministic builders with an explicit unique suffix. Unbounded random input creates failures that cannot be replayed. Property-based testing can add value, but it needs shrinking, seeds, and invariants rather than ad hoc random strings.

11. REST Assured given when then Compared with Other Styles

The BDD-style chain is not the only valid REST Assured syntax, and REST Assured is not the only API testing tool. Choose based on team language, test layer, and diagnostic needs.

Style or tool Best fit Advantage Tradeoff
REST Assured given/when/then Java API integration and system tests Readable fluent DSL with Hamcrest and extraction Long chains can hide abstractions if unmanaged
REST Assured direct get().then() Simple requests without setup Compact Less explicit scenario structure
Java HTTP client plus assertions Low-level protocol or library tests Full transport control More serialization and assertion plumbing
Spring MockMvc module In-process Spring MVC controller tests Fast, no deployed server required Does not exercise the real network stack
Playwright APIRequestContext Mixed browser and API setup in TS suites Shared runner and fixtures Less natural for Java-centric teams
Postman or Newman collections Collaborative examples and lightweight checks Accessible UI and shareable requests Large suites need careful code discipline

Within REST Assured, use the full BDD chain when it clarifies scenario phases. A simple health request can remain get('/health').then().statusCode(200). Consistency should support comprehension, not create ceremony.

Separate contract tests, component integration tests, and deployed end-to-end API tests in reporting. The same DSL can serve each, but their environment, failure ownership, speed, and confidence differ. A MockMvc test that never opens a socket should not be reported as proof that TLS, routing, gateway headers, and deployment configuration work.

The strongest portfolio demonstrates a pyramid of checks: focused controller or component tests, service integration tests with real dependencies or controlled substitutes, contract compatibility checks, and a smaller deployed workflow suite. REST Assured is a client-side tool within that strategy, not the strategy itself.

Interview Questions and Answers

Q: What belongs in given, when, and then?

given contains request prerequisites and data, when dispatches the HTTP operation, and then validates the returned contract. The request is actually sent by the HTTP method call. I keep setup and cleanup outside the fluent chain unless they directly configure the request.

Q: How do you reuse common REST Assured configuration safely?

I build request and response specifications with their builders and apply them using .spec(...). I keep environment values immutable and avoid changing global REST Assured fields in parallel suites. Scenario-specific statuses and body assertions stay in the test.

Q: How do you pass data between API requests?

I validate the first response, call .extract() for the required ID or typed response, and store it in test-scoped state. The next request receives it explicitly as a path parameter, body field, or header. I avoid static mutable state and order-dependent tests.

Q: What do you validate besides status code?

I validate media type, relevant headers, stable body fields, schema where valuable, and the business rule specific to the scenario. For an error, I validate the problem code and safe details. I avoid exact assertions on unrelated volatile timestamps and generated values.

Q: How do you debug REST Assured failures securely?

I enable request and response logging on validation failure, attach a per-request correlation ID, and inspect service traces. I blacklist authorization and cookie headers and avoid logging sensitive bodies. Diagnostic code must preserve the original assertion failure.

Q: How do you make REST Assured tests safe for parallel execution?

I use immutable configuration and specifications, unique per-test data, and local extracted values. I avoid mutable global defaults, shared accounts with conflicting state, and ordered cleanup. Each test owns its resources or uses an isolated namespace.

Common Mistakes

  • Treating an HTTP 200 assertion as complete verification of the API contract.
  • Putting several hidden HTTP requests inside one helper and calling it a single step.
  • Using generic param when queryParam, pathParam, or formParam would state intent.
  • Sharing mutable global base URIs, authentication, or extracted IDs across parallel tests.
  • Logging all headers and bodies in CI without masking credentials and sensitive data.
  • Comparing decimal JSON values with the wrong Java numeric type.
  • Extracting a field before validating that the response is successful and structurally correct.
  • Using fixed sleeps for asynchronous workflows instead of bounded polling.
  • Asserting every volatile field in a golden JSON document and creating noisy maintenance.
  • Making negative tests accept several status codes without confirming the documented security contract.
  • Allowing cleanup failures to hide the original test failure or delete another test's data.

Conclusion

REST Assured given when then is effective because it turns an HTTP exchange into a readable specification: define the request, dispatch it, and validate the response. The syntax is easy to learn, but suite quality comes from accurate protocol assertions, immutable reuse, isolated data, explicit extraction, and secure diagnostics.

Start with one endpoint and write a happy path plus a meaningful negative path. Assert status, content type, important headers, and domain behavior, then refactor only stable repetition into specifications. That progression produces tests that remain readable in code review and useful when a deployment fails.

Interview Questions and Answers

Explain the execution point in a REST Assured given when then chain.

`given()` builds the request specification and `when()` is a readable transition. The request is sent when an HTTP method such as `get` or `post` executes. `then()` wraps the received response for validation.

How would you structure a maintainable REST Assured framework?

I separate immutable environment configuration, small request and response specifications, typed payloads, domain-focused client helpers, and scenario tests. Business-critical assertions remain visible in tests. Reporting and secure failure logging are cross-cutting layers rather than copied code.

Why should assertions happen before extraction?

A value from an error or malformed response should not become setup for later actions. Validating status and structure first localizes the failure to the request that produced bad data. It also prevents misleading downstream errors.

What problems can global RestAssured configuration cause?

Mutable global base URIs, paths, authentication, and specifications can leak between tests and race during parallel execution. One class can silently affect another. I prefer immutable specs applied to each request and reset unavoidable legacy globals.

How do you validate eventually consistent APIs?

I submit the operation, capture its identifier, and poll a documented status or resource endpoint with a bounded deadline and useful interval. I fail with the last response attached. I do not use a single fixed sleep because processing time varies.

How do you test API authorization with REST Assured?

I build a matrix across anonymous, invalid, expired, correctly scoped, insufficiently scoped, and cross-tenant principals. I assert the documented 401 or 403 response, safe error body, and absence of forbidden side effects or data. Tokens come from controlled fixtures and are never printed.

Frequently Asked Questions

What is given when then in REST Assured?

It is REST Assured's BDD-style Java DSL. `given()` configures a request, the HTTP method after `when()` sends it, and `then()` exposes response validation methods.

Is when mandatory in REST Assured?

No. REST Assured supports shorter forms such as `get('/health').then()`. Use `when()` when it makes the request boundary and scenario language clearer.

How do I extract a value after REST Assured assertions?

Chain `.extract().path('field')`, `.extract().response()`, or `.extract().as(Type.class)` after the `then()` assertions. Validate the status and core response contract before using extracted data.

How do I reuse headers and base URLs in REST Assured?

Build a `RequestSpecification` with `RequestSpecBuilder` and apply it through `given().spec(requestSpec)`. This is safer and clearer than repeatedly mutating global defaults, especially in parallel suites.

Can REST Assured test JSON schema?

Yes, through the separate `json-schema-validator` module and its schema matchers. Use schema checks alongside business assertions because a structurally valid response can still contain incorrect data.

How should REST Assured log failed requests?

Use validation-failure logging or a controlled filter rather than always-on full logging. Mask authorization, cookies, tokens, personal data, and sensitive request or response fields.

Does REST Assured support JUnit 5?

Yes. REST Assured supplies the HTTP testing DSL, while JUnit 5 supplies test discovery, lifecycle, parameterization, and assertions or extensions. Add both test dependencies and run them through a compatible build plugin.

Related Guides