given, when, then: your first tests
How a REST Assured test reads, what each part does, and what a failure tells you.
A REST Assured test reads in three parts. given() describes the request, when() sends it, and then()lists what must be true of the response. The words come from behaviour-driven development, but you don't need to know anything about that: they're just a readable way to chain method calls. This lesson goes through the first test class line by line.
The first test class
This is FirstTest.java from the example project, exactly as it runs:
package com.testingapis.examples;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
/** Lesson 2: the given / when / then shape, with nothing shared yet. */
class FirstTest {
@Test
@Tag("smoke")
void productOneHasATitleAndAPrice() {
given()
.baseUri("https://api.testingapis.com")
.when()
.get("/api/Products/1")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("id", equalTo(1))
.body("title", equalTo("Wireless Noise-Cancelling Headphones"))
.body("price", greaterThan(0f));
}
@Test
void aMissingProductReturns404() {
given()
.baseUri("https://api.testingapis.com")
.when()
.get("/api/Products/9999")
.then()
.statusCode(404)
.body("message", containsString("9999"));
}
}The imports
REST Assured is used through static imports, so given() can be written without a class name. The matchers, equalTo, greaterThan and containsString, come from Hamcrest, which REST Assured includes. Your editor will usually offer to add these imports; when it doesn't, it's nearly always because a static import is missing.
The three parts
| Part | Here | Typical contents |
|---|---|---|
given() | .baseUri("https://api.testingapis.com") | Base URL, headers, query and path parameters, auth, body |
when() | .get("/api/Products/1") | The method and path: get, post, put, patch, delete |
then() | .statusCode(200) … .body(…) | Status, headers, content type and body checks |
when() and then()don't do anything themselves; they exist to make the test read well. The request is sent by .get(...). The indentation, with given, when and thenat one level and their details below, isn't required either, but it's the convention, and it makes long tests far easier to scan.
Checks on the body
.body(path, matcher) reaches into the JSON response by path and applies a Hamcrest matcher. "id" is the top-level id field; lesson 3 covers nested fields and lists. Each check is independent, so a test can hold several:
.then()
.statusCode(200)
.body("id", equalTo(1))
.body("title", equalTo("Wireless Noise-Cancelling Headphones"))Each .body()call is checked as it's reached, so the first one that fails ends the test and you never hear about the rest. When you want every mismatch at once, pass several path and matcher pairs to a single .body(). Both versions against product 1, with two deliberately wrong expectations:
// Separate .body() calls: the run stops at the first one that fails
.body("id", equalTo(2))
.body("category", equalTo("books"))
1 expectation failed.
JSON path id doesn't match.
Expected: <2>
Actual: <1>
// The same two checks in one .body() call: both are reported
.body("id", equalTo(2), "category", equalTo("books"))
2 expectations failed.
JSON path id doesn't match.
Expected: <2>
Actual: <1>
JSON path category doesn't match.
Expected: books
Actual: electronicsThe single call reported both problems in one run. For a few checks the difference hardly matters; for a test that compares ten fields of a record, seeing every wrong field at once saves several rounds of fix, run, fix again.
Testing a failure on purpose
The second test asks for a product that doesn't exist and expects a 404 with the ID in the message. REST Assured doesn't treat a 4xx or 5xx response as an error: it's just a response, and your statusCodecheck decides what's right. That's what makes negative tests this short.
Reading a failure
Here is a real failing check, from a test that expected the health endpoint to say up:
[ERROR] com.testingapis.examples.SpecsTest.healthCheck -- Time elapsed: 3.747 s <<< FAILURE!
java.lang.AssertionError:
1 expectation failed.
JSON path status doesn't match.
Expected: up
Actual: healthyThe path that failed, what was expected and what came back. The example project also turns on REST Assured's logging for failed tests only, so the same output includes the whole request and response:
Request method: GET
Request URI: https://api.testingapis.com/api/health
Headers: Accept=application/json, application/javascript, text/javascript, text/json
Body: <none>
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
...
{
"status": "healthy",
"uptimeSeconds": 84939,
"timestamp": "2026-10-11T14:01:55Z"
}That's usually enough to see the problem without rerunning anything. Lesson 6 shows where the setting lives. Notice the Accept header too: asking for ContentType.JSON sends four media types, not just application/json. Most APIs don't mind; one that checks Accept strictly might.
Habits from the start
- Check the status first, then the content type, then the body.
- Name test methods as sentences about the API, like
aMissingProductReturns404. - Write at least one negative test for every positive one.
- Keep one request per test unless the test is about a sequence, like the chains in lesson 4.
Exercise
Add a test to FirstTest for GET /api/Todos/1 that checks the status, that the content type is JSON, and that completed is a boolean. You'll need a matcher for the type; Hamcrest's instanceOf(Boolean.class) is one option. Then make each check fail in turn and read the message REST Assured gives.