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 ingiven().when()— the action. Exactly one HTTP call:.get(path),.post(path),.put(path),.patch(path),.delete(path). The chain is built so that anything afterwhen()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. Afterwhen(), you're operating on the response andbody(...)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 sharedRequestSpecification). 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.
- In your Maven project from Lesson 1, create
tests/JsonPlaceholderTest.java. SetRestAssured.baseURI = "https://jsonplaceholder.typicode.com"in@BeforeClass. - Write one
@Testthat GETs/usersand asserts the status is200and the response body is a list of size10. Run it green. - Write a second
@Testthat GETs/users/1and asserts the body'snamefield is"Leanne Graham"andemailis"Sincere@april.biz". Run it green. Note how eachbody(...)assertion is independent — both fail messages would show up. - Force a failure on purpose. Change one assertion to expect
nameequal to"Bogus". Run again. Read the failure message — it shows expected vs actual. Restore. - 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. - Refactor. Replace the
@BeforeClassbody with a singleRequestSpecification-style approach: build one withRequestSpecBuilderand call it.spec(commonSpec)in yourgiven(). We'll formalise this in Chapter 6, but try the shape now to feel the win. - 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.