skip to content

Status and Error Paths

How a test states an expectation about the status code, and what REST Assured actually prints when that expectation is wrong. An undiagnosable red build in CI usually starts on this one line.

part ofREST Assuredoverview, primer and where to startread it →
on this pageshow

questions

4

In REST Assured, why can a test of a rejected request pass even when the server returns 200?

level: juniorimportance: must knowfreq 66%

answer

  1. no expectation, no validation
  2. one gate decides whether anything runs
  3. onFailMessage alone asserts nothing
  4. name the code you expect to be refused

basics

~20 s

REST Assured validates only when at least one expectation exists. A call with no then() block, or a then() that sets no status, status line, body, header, cookie, content type or time check, returns the response and never fails.

solid answer

~40 s

REST Assured's response specification gates all validation behind `hasAssertionsDefined()`, which is true only once you have recorded an expected status code, an expected status line, a content type, a response-time matcher, or a header, cookie or body matcher. Nothing else counts: `onFailMessage(...)` does not, the logging calls do not, and neither do `assertThat()` and `and()`, which merely return the same object. So `given().body(json).when().post("/call-sheets/17/crew-calls")` hands back a `Response` and ends green whatever the server said, and a `then()` chain that only prints does the same. A negative case has to name the outcome it expects — `then().statusCode(409)` for the duplicate crew call, plus a claim about the error payload if the contract has one. Write the assertion, then watch it fail against the happy path before you trust it.

code

java · 17 lines
java
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

String duplicate = "{\"crewMemberId\":\"c-7741\",\"callTime\":\"17:30\"}";

// Asserts nothing: the Response comes back and the method ends green.
given().contentType("application/json").body(duplicate)
.when()
    .post("/call-sheets/17/crew-calls");

// States the refusal, so a 201 or a 500 both turn the case red.
given().contentType("application/json").body(duplicate)
.when()
    .post("/call-sheets/17/crew-calls")
.then()
    .statusCode(409)
    .body("errorCode", equalTo("CREW_CALL_DUPLICATE"));

go deeper

for a junior

Be ready to write the rejection case yourself: state the status you expect and one claim about the error payload. Know that a chain carrying no expectation never fails.

for a middle

Explain the gate — REST Assured validates only once a status, status line, content type, response time, header, cookie or body expectation has been recorded — and name what does not count.

for a senior

Show how you find silent cases in an inherited suite and how you prove an assertion is live: change the fixture or the expected code deliberately and confirm the case turns red.

for a principal

Own the convention that makes silence impossible — helpers that carry an outcome expectation by construction — and decide who enforces it and how an exception gets approved.

## What actually triggers a check REST Assured never validates a response speculatively. Everything runs behind one gate on the response specification, `hasAssertionsDefined()`, which is true only once at least one of these has been recorded: - an **expected status code**, from `statusCode(int)` or `statusCode(Matcher)` - an **expected status line**, from `statusLine(String)` or `statusLine(Matcher)` - an **expected content type** - an **expected response time** - a **header** or **cookie** assertion - one or more **body matchers** If none of those exists, the assertion closure returns immediately and the call is over. That single fact explains the whole failure mode: a request that is sent and never claimed about cannot go red, whatever the stage-crew service answered. ## Why a silent pass is easy to write The DSL makes the silent form shorter than the asserting one, which is exactly backwards from what you want: - `given().body(json).when().post("/call-sheets/17/crew-calls");` is a complete, legal statement. It returns a `Response` and asserts nothing. - A helper that ends in `.extract().response()` and hands the `Response` back to the caller has also asserted nothing unless the caller does. - `then()` on its own is not an assertion. Neither are `assertThat()` and `and()`, which are syntactic sugar returning the same object. - `onFailMessage("duplicate crew call")` is a setter for a failure label. It is not in the gate list, so a `then()` chain containing only that validates nothing. - A `then()` chain that only prints is in the same position: printing is not claiming. None of these throw, none warn, and none show up as a skipped case in a report. The suite is green because it asked nothing. ## Naming the rejection A negative case has to say what "rejected" means in terms the runner can check. On the call-sheet API, the duplicate-crew-call case is: ```java given() .contentType("application/json") .body(duplicateCrewCall) .when() .post("/call-sheets/17/crew-calls") .then() .statusCode(409) .body("errorCode", equalTo("CREW_CALL_DUPLICATE")); ``` Two claims, not one. The status alone would also be satisfied by a `409` raised for an unrelated reason — an optimistic-lock clash on the call sheet, say — so the case would pass while proving nothing about duplicate detection. Pairing the status with one stable field of the error payload pins the *reason*, and both are recorded on the same specification, so both are checked. ## What you get when it does fail Once at least one expectation exists, a failing check throws a single `AssertionError` whose first line counts the failures and whose body is each mismatch in turn: ``` 2 expectations failed. Expected status code <409> but was <201>. JSON path errorCode doesn't match. Expected: "CREW_CALL_DUPLICATE" Actual: null ``` That is the whole point of writing the expectation down: the message names the code you wanted, the code you got, and the field that disagreed. A silent case gives you none of it, and the first sign that anything is wrong arrives from production. Laid side by side, the three shapes a "negative" case can take differ only in what they claim: | What you wrote | What a wrong 201 does | |---|---| | `when().post(path)` with no `then()` | nothing; the `Response` is returned and the case is green | | `then()` carrying only a label or a print | nothing; neither one is an expectation | | `then().statusCode(409)` | throws `Expected status code <409> but was <201>.` | Only the third row is a test. The first two are requests with an opinion nobody wrote down, and they cost exactly as much to run. ## A checklist for a negative case 1. **State the status.** `statusCode(409)` — an exact code unless the contract truly admits more. 2. **State the reason.** One assertion over a stable machine-readable field of the error body, not over a human-readable sentence that a copy edit will break. 3. **Prove the assertion is live.** Point the same case at the happy-path fixture, or flip the expected code, and confirm it goes red. An assertion you have never seen fail is not evidence. 4. **Never rely on absence of an exception.** "The call did not throw" is true of every silent case in the suite. 5. **Check the helpers.** If a shared helper returns a `Response` rather than a validated one, the claim has to be made at the call site every single time — and eventually will not be. ## The one-line summary REST Assured is not an assertion framework that runs by default; it is a client that validates only what you ask it to. **No expectation, no validation, no failure** — and the suite reports the same green either way.

  • Does then().onFailMessage("...") on its own make a REST Assured call fail when the server errors?
    No. `onFailMessage` only stores a string to append to a failure message; it is not one of the things `hasAssertionsDefined()` looks for. A chain of `then().onFailMessage("duplicate crew call")` and nothing else validates nothing at all and passes on any response. Pair it with a real expectation such as `statusCode(409)`.
  • How would you stop a whole suite from silently accepting whatever the call-sheet service returns?
    Make the expected outcome structural rather than optional: shared helpers that return an already-validated response instead of a bare `Response`, or a `ResponseSpecification` attached with `then().spec(...)`, and a review rule for any case that opts out. The cheap proof is mutation — make the service answer the wrong code and confirm the case turns red.

saying these in an interview costs you the question

  • Thinks a bare when().post(...) fails on a 500
  • Counts onFailMessage or a log call as an assertion
  • Asserts only that the call did not throw an exception
  • Writes then() with no expectation and calls the path covered
  • Assumes any 4xx proves the intended rejection happened
open as a page

In REST Assured, why does then().statusCode(200).onFailMessage("...") never print your message?

level: middleimportance: should knowfreq 38%

basics

~20 s

Every expectation on a then() chain validates the moment it is called, so statusCode(200) throws before onFailMessage is reached and the message field is still unset. Put onFailMessage first, immediately after then(), ahead of any expectation it should annotate.

open as a page

In REST Assured, what does then().statusCode(anyOf(is(409), is(422))) allow that statusCode(409) does not?

level: middleimportance: should knowfreq 58%

basics

~20 s

The matcher overload accepts a set or range of codes instead of one exact value, so a contract that legitimately answers with either 409 or 422 still passes. It also changes the failure text, which then names every accepted code.

open as a page

In REST Assured, what can a FailureConfig failure listener do with a response that then() rejected?

level: seniorimportance: should knowfreq 32%

basics

~20 s

A failure listener receives the request specification, the response specification and the full Response just before the AssertionError is thrown, so it can record or forward them. It cannot change the verdict, edit the message, or retry.

open as a page