The Given-When-Then Syntax

8 min read

given().when().then() is Rest Assured's signature look. It mirrors the Behaviour-Driven Development convention you'll have seen in Cucumber and Gherkin: given a starting state, when an action happens, then assert the outcome. The chain is more than cosmetic — each block has a specific job, and writing tests that respect those jobs keeps a 200-test suite legible to anyone who joins the team. This lesson is the deep dive on the chain: what goes where, how it composes, and the small set of configuration moves that prevent you from repeating yourself in every test.

The three blocks

given()
    .baseUri("https://api.example.com")
    .header("Content-Type", "application/json")
    .queryParam("role", "admin")
.when()
    .get("/users")
.then()
    .statusCode(200)
    .body("users.size()", greaterThan(0));

Top to bottom, each block answers a different question:

  • given() — preconditions. Everything the request needs before it goes out: base URI, headers, authentication, query/path/form parameters, request body, cookies, multipart parts. If you'd write it on a Postman tab before clicking Send, it belongs in given().
  • when() — the action. Exactly one HTTP call: .get(path), .post(path), .put(path), .patch(path), .delete(path). The chain is built so that anything after when() is talking about the response, not the request.
  • then() — the assertions. Status code, response headers, response body fields, response time. Combine as many assertions as you want; each one fails the test on its own.

The keyword methods themselves are also chainable connectors: .and(), .with() — semantically null but visually helpful in long chains. None of the keywords (given, when, then, and) is technically required; they're connector methods that return the same RequestSpecification or ValidatableResponse. Adopting them gives you the BDD readability for free.

Reading a chain top to bottom

The fluent style only earns its keep if you write it so it reads in order. A test that mixes setup, action, and assertions across the chain is harder to read than one that doesn't. Compare:

// Hard to scan — assertion before action, body after when()
given()
    .header("Accept", "application/json")
    .body(jsonPayload)
    .when()
    .post("/users")
    .then()
    .statusCode(201);
// Easy to scan — every block does its job
given()
    .header("Accept", "application/json")
    .contentType(ContentType.JSON)
    .body(jsonPayload)
.when()
    .post("/users")
.then()
    .statusCode(201)
    .body("id", notNullValue());

The second version is a specification. The first is plumbing. Both work; only one is reviewable in a hurry.

The simplified form (when you don't need setup)

For one-off, no-setup requests, you can skip given() entirely:

get("https://api.example.com/users")
    .then()
    .statusCode(200);

get() here is a static method on RestAssured that's also a static import. It returns a Response directly, and .then() validates against it. Use this when there's literally nothing to set up — usually only in throwaway scripts. In a real suite you'll almost always want given() so you can attach a base URI, auth, or default headers.

The flow, visualised

Step 1 of 5

given()

Configure the request — base URI, headers, auth, path/query parameters, body. Nothing has hit the network yet; you're building the RequestSpecification.

The chain is sequential by design. There's no asynchronous step you have to await; Rest Assured blocks until the response is in hand and assertions can run.

Setting the base URI globally

In a real suite, every test hits the same host. Repeating .baseUri("https://api.example.com") in 200 tests is the textbook DRY violation. Set it once:

import io.restassured.RestAssured;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
 
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
 
public class UsersTest {
 
    @BeforeClass
    public void setup() {
        RestAssured.baseURI = "https://api.example.com";
        RestAssured.basePath = "/api/v1";
    }
 
    @Test
    public void getUsersReturns200() {
        given()
        .when()
            .get("/users")    // resolves to https://api.example.com/api/v1/users
        .then()
            .statusCode(200);
    }
}

RestAssured also has a defaultRequestSpecification that lets you preset headers, content types, auth, and even logging — we'll formalise this in Chapter 6 when we build a BaseTest class. For now, the global baseURI is the smallest move that gives the biggest win.

The console output, when a test fails

When statusCode(200) fails because the API returned 500, Rest Assured prints something like:

java.lang.AssertionError: 1 expectation failed.
Expected status code <200> but was <500>.

	at io.restassured.internal.ValidatableResponseImpl ...

Helpful but not enough — you usually want the body of the failing response too. The neat fix:

RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();

Drop that in @BeforeSuite and Rest Assured silently captures every request and response, then dumps them only when an assertion fails. Quiet on success, loud on failure — the perfect default. We'll come back to logging filters in Chapter 6.

Comparing with other tools

The BDD chain is unique to Rest Assured. Here's how the same test looks in three different tool families:

// Postman pm.test() inside the Tests tab of a request
pm.test("Status is 200", () => pm.response.to.have.status(200));
pm.test("Has users", () => pm.expect(pm.response.json().users.length).to.be.greaterThan(0));
# Python requests + pytest
import requests
 
def test_get_users():
    r = requests.get("https://api.example.com/users", params={"role": "admin"})
    assert r.status_code == 200
    assert len(r.json()["users"]) > 0
// Rest Assured
given()
    .baseUri("https://api.example.com")
    .queryParam("role", "admin")
.when()
    .get("/users")
.then()
    .statusCode(200)
    .body("users.size()", greaterThan(0));

Three languages, three idioms. Postman ties tests to a collection. Python uses imperative assertions. Rest Assured leans into the BDD chain. Once you've written one, you can read all three — and on a Java team, the BDD chain reads more like a product specification than the imperative alternative.

Why the chain matters in code review

A reviewer can scan a 30-test class and answer "what does this suite cover?" in under a minute only if every test follows the same shape. Mixed paradigms — some BDD chains, some imperative Response r = ...; assertEquals(...) — slow review and, over time, hide bugs. Pick the BDD chain, write a one-page convention doc, and enforce it with a simplify-style review. The cost of the convention is one paragraph; the payoff is a suite that scales.

⚠️ Common mistakes

  • Putting setup after when(). Calls like .body(payload) belong before .when(), in the request configuration block. After when(), you're operating on the response and body(...) means assert on the response body. Putting them in the wrong order either fails to compile or asserts on the wrong thing.
  • Repeating the base URI in every test. Set it once in @BeforeClass/@BeforeSuite (or via a shared RequestSpecification). Tests that hardcode the host can't be re-pointed at a different environment without a find-and-replace.
  • Not enabling failure logs. When CI fails with "Expected 200 but was 500" and no body, you'll spend 20 minutes reproducing locally to see the response. enableLoggingOfRequestAndResponseIfValidationFails() gives you the body for free, every time, with zero noise on green runs.

🎯 Practice task

Run the chain three different ways and feel how each block behaves. 30 minutes. Use JSONPlaceholder — a free fake API that's perfect for these drills.

  1. In your Maven project from Lesson 1, create tests/JsonPlaceholderTest.java. Set RestAssured.baseURI = "https://jsonplaceholder.typicode.com" in @BeforeClass.
  2. Write one @Test that GETs /users and asserts the status is 200 and the response body is a list of size 10. Run it green.
  3. Write a second @Test that GETs /users/1 and asserts the body's name field is "Leanne Graham" and email is "Sincere@april.biz". Run it green. Note how each body(...) assertion is independent — both fail messages would show up.
  4. Force a failure on purpose. Change one assertion to expect name equal to "Bogus". Run again. Read the failure message — it shows expected vs actual. Restore.
  5. Add fail-mode logging. Add RestAssured.enableLoggingOfRequestAndResponseIfValidationFails() in @BeforeClass. Force the failure again. Now the full request and response dump above the assertion error. Notice how much faster you can debug.
  6. Refactor. Replace the @BeforeClass body with a single RequestSpecification-style approach: build one with RequestSpecBuilder and call it .spec(commonSpec) in your given(). We'll formalise this in Chapter 6, but try the shape now to feel the win.
  7. Stretch: convert one test to the simplified form get("https://...").then().statusCode(200);. Compare readability with the full chain. When does the short form earn its keep? When does it cost you?

Next lesson: your first complete GET and POST, including extracting fields from the response so the next request can use them.

// tip to track lessons you complete and pick up where you left off across devices.