In REST Assured, why can a test of a rejected request pass even when the server returns 200?
answer
- no expectation, no validation
- one gate decides whether anything runs
- onFailMessage alone asserts nothing
- name the code you expect to be refused
basics
~20 sREST 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 sREST 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 linesimport 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
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.
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.
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.
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