In REST Assured, what does then().statusCode(anyOf(is(409), is(422))) allow that statusCode(409) does not?
answer
- one stored field, always a matcher
- the int form wraps equalTo
- a wide bound hides a 500
- message reads describeTo then describeMismatch
basics
~20 sThe 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.
solid answer
~40 s`ValidatableResponse.statusCode(int)` is not a separate code path — it is literally `statusCode(equalTo(expected))`, so REST Assured always stores a Hamcrest `Matcher<? super Integer>` and always compares through it. Passing your own matcher, such as `anyOf(is(409), is(422))` or `allOf(greaterThanOrEqualTo(200), lessThan(300))`, just replaces `equalTo` with something looser. Reach for that only when the contract genuinely admits more than one answer — a duplicate `POST /call-sheets/17/crew-calls` that one service refuses as 409 and another as 422. A wide bound such as `greaterThanOrEqualTo(400)` turns a 500 regression into a green test. The failure text follows the matcher, because REST Assured builds it from `describeTo` plus `describeMismatch`: `statusCode(201)` prints `Expected status code <201> but was <500>.`, while the `anyOf` form prints `Expected status code (is <409> or is <422>) but was <500>.`
code
java · 19 linesimport static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is;
String duplicate = "{\"crewMemberId\":\"c-7741\",\"callTime\":\"17:30\"}";
// Exact: this service always refuses a duplicate crew call with 409.
given().contentType("application/json").body(duplicate)
.when()
.post("/call-sheets/17/crew-calls")
.then()
.statusCode(409);
// Enumerated set: the gateway in front may answer 409 or 422 instead.
given().contentType("application/json").body(duplicate)
.when()
.post("/call-sheets/17/crew-calls")
.then()
.statusCode(anyOf(is(409), is(422)));go deeper
Be ready to write both forms and say what each compares. Know that statusCode takes either a plain int or a Hamcrest matcher, and that 409 written as an int means exactly 409.
Explain that the int overload is implemented as equalTo, so there is one comparison path and one stored matcher. Show how the failure text is generated from the matcher's own description.
Show judgment about how loose an accepted-code matcher may be. Argue why a bound spanning a whole status class lets a server regression ship green, and where a two-code anyOf really is the contract.
Own the convention across a suite: whether accepted-code sets are allowed at all, where they live so a reviewer can see them, and how you stop a wide matcher spreading by copy-paste.
## The two overloads are one mechanism `io.restassured.response.ValidatableResponseOptions` — the interface behind whatever `then()` hands you — declares exactly four status members: `statusCode(int)`, `statusCode(Matcher<? super Integer>)`, `statusLine(String)` and `statusLine(Matcher<? super String>)`. The `int` form is **not** a shortcut past Hamcrest. Inside `ResponseSpecificationImpl` it is written as `return statusCode(equalTo(expectedStatusCode))`, so there is one stored field, `expectedStatusCode`, and it always holds a matcher. Validation is one comparison: if `!expectedStatusCode.matches(actual)` the check records an error. The real question is therefore never "matcher or no matcher" — it is **which matcher**, and the `int` overload picks `equalTo` on your behalf. `statusCode(anyOf(is(409), is(422)))` swaps that for a disjunction; `statusCode(allOf(greaterThanOrEqualTo(200), lessThan(300)))` swaps it for a range. ## When a looser matcher is honest A wider matcher is a weaker claim, and a weaker claim is worth making only when the contract is genuinely wider. On a stage-crew call-sheet service the cases divide cleanly: - **Exact.** `POST /call-sheets` answers `201` on success. Write `statusCode(201)`; nothing else is correct behaviour, so nothing else should be green. - **Genuinely two-valued.** A duplicate crew call on `POST /call-sheets/17/crew-calls` may be refused as `409` or `422` depending on which validator catches it first. `anyOf(is(409), is(422))` states that honestly. - **A class you do not control.** A gateway in front of the service may answer `502` or `504` on its own. If a case must tolerate that, say so with a named set, not with a range. - **Never a whole family.** `greaterThanOrEqualTo(400)` says "anything went wrong", which is the one thing the case was not built to assert; a 500 regression then ships green. The rule of thumb: a matcher that would still pass after a real defect is not an assertion, it is documentation. ## What the failure message actually says REST Assured builds the status-code error through `MatcherErrorMessageBuilder`, which appends `"Expected status code "`, then the matcher's own `describeTo`, then `" but "`, then the matcher's `describeMismatch`. The matcher you chose is literally what you read on a red build: | Expectation | Message on an actual 500 | |---|---| | `statusCode(201)` | `Expected status code <201> but was <500>.` | | `statusCode(is(201))` | `Expected status code is <201> but was <500>.` | | `statusCode(anyOf(is(409), is(422)))` | `Expected status code (is <409> or is <422>) but was <500>.` | Two consequences follow. First, a well-chosen matcher documents the contract in the failure output for free. Second, **the message carries no response body** — the default `describeMismatch` on a Hamcrest matcher prints only `was <500>`, so a status-only assertion tells you the number and nothing about why. Every failed check in one specification is collected, not fail-fast within that pass: the errors are joined and thrown as a single `AssertionError` whose first line counts them — `1 expectation failed.` or `3 expectations failed.` — with status and status line validated before headers, content type, response time and body matchers. ## statusLine is a different surface `statusLine(String)` is likewise `statusLine(equalTo(...))`, but it compares against `Response.statusLine()` — the whole line the client parsed, protocol token and reason phrase included, such as `HTTP/1.1 409 Conflict`. Its mismatch text is built by a plain format string rather than by the matcher builder, so it reads `Expected status line "300" doesn't match actual status line "HTTP/1.1 200 OK".` and never carries a Hamcrest expected/actual block. That makes it a brittle check: it binds the case to a protocol version and to a reason phrase the server is free to change. Prefer `statusCode(...)`, which compares an integer, and fall back to `statusLine(containsString("409"))` only when the line itself is the thing under test. ## Practical rules 1. Default to `statusCode(int)`. It is the narrowest claim and the cheapest to read. 2. Reach for a matcher only when you can name every code it admits and say why each is correct. 3. Prefer `anyOf(is(a), is(b))` over an open-ended bound — an enumerated set cannot silently widen. 4. Do not use a status matcher to paper over flakiness; a case that sometimes gets 409 and sometimes 500 has a problem the assertion cannot fix. 5. Keep `statusLine(...)` out of ordinary cases; assert the number, not the sentence. ## Getting it wrong in review The two failures worth catching are the same failure at different scales. One case written with `greaterThanOrEqualTo(400)` is a small hole. The same line copied across a hundred negative cases is a suite that cannot distinguish a correct refusal from an outage, and the only symptom is that nothing ever goes red. Ask of any status matcher: **which real defect would this still pass?**
- How does then().statusLine("HTTP/1.1 409 Conflict") compare against the response, and why is it brittle?`statusLine(String)` is `statusLine(equalTo(...))` — exact equality against the whole line the client parsed, protocol token and reason phrase included. A server that answers on a different HTTP version, or a framework that swaps `Conflict` for a custom phrase, turns the case red with no behaviour change. Use `statusLine(containsString("409"))` when you need the line at all, and prefer `statusCode(409)`, which compares only the number.
- What does the message look like when the status code and a body expectation fail in the same specification?REST Assured collects every failed check, joins their messages and throws one `AssertionError` opening with a count — `3 expectations failed.` — then each message separated by a blank line. The status-code error comes first, because status and status line are validated ahead of headers, content type, response time and body matchers.
saying these in an interview costs you the question
- Thinks statusCode(int) skips Hamcrest entirely
- Uses greaterThanOrEqualTo(400) so any error code passes
- Believes the matcher overload changes what is thrown
- Asserts the status line when only the code matters
- Expects the status failure message to include the response body