REST Assured ships no retry or timeout DSL - how would you wire waiting across a large suite?
answer
- two absences, two different layers
- config bounds the attempt, a helper the wait
- shared spec beats the global static
- no helper catches AssertionError
basics
~20 sREST Assured leaves two gaps and a suite should fill them separately: a per-attempt time bound carried by one shared RestAssuredConfig, and a readiness wait in one helper that extracts values while the real expectations stay outside it.
solid answer
~50 sTwo different absences, wired in two different places. The per-attempt bound is configuration: build one `RestAssuredConfig` carrying an `HttpClientConfig` and attach it to a shared `RequestSpecBuilder` with `setConfig(...)`, so every call inherits it through `given().spec(fares)`. Prefer that over assigning `RestAssured.config` from a test — the static is global mutable state that leaks across parallel classes, and `new RequestSpecBuilder()` snapshots the statics in its constructor, so a builder created earlier never sees a later assignment. The readiness wait is a helper rather than configuration: it takes the spec, calls REST Assured without the eventual expectation, returns the value, and the caller makes one validating call afterwards. The rule I would enforce in review is that no wait helper catches `AssertionError`. How long to wait, and where a retry belongs in a run, are decisions the library has no opinion on at all.
code
java · 24 linesimport io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import static io.restassured.RestAssured.given;
import static io.restassured.config.HttpClientConfig.httpClientConfig;
import static io.restassured.config.RestAssuredConfig.newConfig;
// layer one: one bounded spec, built once, attached to every call
RequestSpecification fares = new RequestSpecBuilder()
.setBaseUri("https://fares.tramnet.test")
.setConfig(newConfig().httpClient(httpClientConfig()
.setParam("http.connection.timeout", 2_000)
.setParam("http.socket.timeout", 5_000)))
.build();
// layer two: the wait reads and returns; it never asserts, never catches
String status = "PENDING";
long deadline = System.currentTimeMillis() + 20_000L;
while ("PENDING".equals(status) && System.currentTimeMillis() < deadline) {
Thread.sleep(500L);
status = given().spec(fares).pathParam("inspectionId", inspectionId)
.when().get("/inspections/{inspectionId}")
.then().statusCode(200).extract().path("status");
}go deeper
Know that a shared request specification is how a suite gives every test the same base URI and configuration, and that waiting is separate code rather than a library setting.
Explain the two layers concretely: a RestAssuredConfig carrying HttpClientConfig for the attempt, and a helper for the repetition. Say why the two must not be collapsed into one number.
Show the construction-order and parallelism traps around RestAssured.config and RequestSpecBuilder, and the review rule that a wait helper must never catch AssertionError.
Own the whole shape: what the harness provides, what it forbids, what it measures about waiting, and why a blanket retry around REST Assured calls is the wrong answer to a reporting problem.
## Name the two absences separately The single biggest mistake a suite makes here is treating "REST Assured has no timeout" and "REST Assured has no wait" as one problem. They are not, and they are solved in different layers. | Absence | What it bounds | Where it is fixed | |---|---|---| | No `timeout()` on the DSL | one HTTP attempt | `HttpClientConfig` inside a `RestAssuredConfig` | | No retry or polling | how many attempts, and for how long | a helper you write, or a waiting library | Confusing them produces the two failure modes you see in real suites: a five-minute read timeout used as a readiness deadline, or a wait loop wrapped around calls that themselves have no bound and can hang forever inside a single attempt. ## Layer one: the bound, as configuration Build the config once and let it travel with the request specification: - One `RestAssuredConfig` with `httpClient(httpClientConfig().setParam("http.connection.timeout", ...).setParam("http.socket.timeout", ...))`. - Attached with `new RequestSpecBuilder().setConfig(config)`, built once, exposed as one shared `RequestSpecification` the tests use as `given().spec(fares)`. - Not assigned to `RestAssured.config` from inside a test. That static is JVM-wide mutable state; once classes run in parallel, one class's assignment silently changes another's requests. There is a second trap in the same place: `new RequestSpecBuilder()` snapshots the `RestAssured` statics in its constructor, so a builder constructed before you assign `RestAssured.config` will never see that assignment — a bug that reads as "my config is being ignored". - `given().config(...)` on one call stays available as the deliberate local override. ## Layer two: the wait, as one helper The helper's contract is what matters, not its cleverness. Three properties are worth insisting on: 1. **It returns a value; it does not assert.** Inside it, REST Assured validates only invariants — `then().statusCode(200)` on a tram inspection status resource is true whether the inspection is `PENDING` or `SETTLED` — and reads the changing field with `extract().path("status")`. The caller then makes one validating call so the failure message names the mismatch and not the clock. 2. **It never catches `AssertionError`.** A helper that does converts every server error, renamed field and content-type change into "still waiting", and the suite reports timeouts where it should report defects. This is the rule I would actually write into the review checklist, because it is the one that gets violated. 3. **It is one helper, not one per team.** A library with no waiting primitive means everybody invents one; the value of centralising is not elegance, it is that the swallow-nothing rule has a single place to live. ## What I would deliberately not build - **A blanket retry wrapper around every REST Assured call.** REST Assured's failures arrive as `AssertionError`, which a naive wrapper cannot distinguish from a genuine assertion failure, so a wrapper like that re-runs real defects until one attempt happens to pass. Where a retry legitimately sits in a run is a framework-architecture question with its own answer; it is not something to bolt onto the HTTP layer because that is where the code happens to be. - **A house `timeout()` DSL extension.** Wrapping the chain to add a method that looks like library API teaches everyone a call that does not exist, and it hides which of the two absences is being addressed. - **A shared "poll until 200" utility.** Status is the wrong readiness signal for most asynchronous resources; a fare inspection answers `200` immediately and only its `status` field changes. Baking a status-code wait into the harness pushes every team toward the weakest possible signal. ## What I would measure A suite that waits should be able to answer three questions from its own output: - How long each wait actually took, not just whether it passed. REST Assured appends a `TimingFilter` when you have not registered one, so per-attempt timings exist; aggregating a helper's total wait is cheap. - How often a wait consumed most of its deadline. That is the early warning that the service, not the test, is degrading. - Whether any wait ever ended in a timeout rather than in a value. With the swallow-nothing rule in place, a timeout means the resource genuinely never arrived, which is a real finding rather than noise. ## The honest summary for an interview REST Assured's silence on this is a scope decision, not a gap to patch over. It builds requests and it makes assertions, and both of those it does well. A large suite should therefore make the two things it does not do explicit and singular: one configuration object that bounds an attempt, one helper that bounds the waiting, and a hard rule that neither of them ever hides a failure the library was trying to report.
- Why not just assign RestAssured.config once in a static initialiser and be done?Because it is JVM-wide mutable state. Once test classes run in parallel, any class that reassigns it changes everyone else's requests, and failures land in tests that never touched the config. There is also a construction-order trap: `new RequestSpecBuilder()` snapshots the statics, so a builder created before the assignment never sees it. A shared specification keeps the configuration with the request.
- What would you write into a review checklist for wait helpers in a REST Assured suite?One line: no wait helper catches `AssertionError`. It must read a value and return it, leaving the caller to make a single validating call. That rule alone prevents the failure mode where every server error, renamed field and content-type change is reported as a timeout instead of as the defect it is.
- How would you stop teams inventing a house timeout() DSL extension?Give them the thing they actually want: a shared bounded specification they get for free through `given().spec(...)`, so nobody needs to reach for a bound per call. A wrapper that adds a method looking like library API teaches a call that does not exist and hides which of the two absences is being addressed.
saying these in an interview costs you the question
- Proposes a house timeout() wrapper that looks like real library API
- Wraps every REST Assured call in a blanket retry
- Assigns RestAssured.config from individual tests and calls it shared setup
- Uses a long read timeout as the readiness deadline
- Lets the shared wait helper catch AssertionError to keep going