skip to content

A REST Assured ResponseSpecBuilder loses its expectStatusCode(200) after addResponseSpecification(...) — why?

level: seniorimportance: should knowfreq 27%

answer

  1. the argument wins, not the builder
  2. scalars assigned, collections appended
  3. an unset field still overwrites
  4. merge first, then layer your scalars

basics

~20 s

ResponseSpecBuilder.addResponseSpecification runs the response-side merge, which assigns the incoming spec's scalar fields unconditionally. Because that spec never set a status code, its null value is written over your 200. Only body, header and cookie assertions accumulate.

solid answer

~40 s

`addResponseSpecification(other)` does not layer `other` on top of what the builder already holds — it runs REST Assured's response-side merge with the builder's spec as the target and `other` as the source, and the source wins on every scalar. Status code, status line, content type, expected response time, response log detail, fallback parser and body root path are assigned straight across, whether or not `other` ever set them. A spec built only to add `expectBody("quotaRemainingKg", greaterThan(0))` therefore carries a null status code, and merging it wipes the `expectStatusCode(200)` you set a line earlier. Only body matchers, header assertions, cookie assertions and registered parsers are added to. The cure is call order: merge the shared spec into an **empty** builder first and layer endpoint-specific expectations on top, or re-declare the scalar after the merge.

code

java · 20 lines
java
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.specification.ResponseSpecification;

import static org.hamcrest.Matchers.greaterThan;

ResponseSpecification bodyOnly = new ResponseSpecBuilder()
        .expectBody("quotaRemainingKg", greaterThan(0))
        .build();

// broken: bodyOnly has no status code, and the merge writes that null over 200
ResponseSpecification broken = new ResponseSpecBuilder()
        .expectStatusCode(200)
        .addResponseSpecification(bodyOnly)
        .build();

// safe: merge into an empty builder, then set the scalars afterwards
ResponseSpecification safe = new ResponseSpecBuilder()
        .addResponseSpecification(bodyOnly)
        .expectStatusCode(200)
        .build();

go deeper

for a junior

Know that a REST Assured response spec can absorb another one with addResponseSpecification, and that the two are not simply added together — some settings from the argument are copied over yours.

for a middle

Explain the merge in terms of fields: scalars are assigned from the incoming spec, collections are appended to. Then explain why a scalar the incoming spec never set is the dangerous case rather than a harmless one.

for a senior

This is a silent-pass bug, so demonstrate how you would catch it. A check that no longer runs looks exactly like a check that passes; describe the deliberately-wrong response you would point the spec at to tell them apart.

for a principal

Decide whether spec composition earns its place at all. One flat spec per service is harder to reuse and much harder to break; layered specs need an explicit rule about which layer owns each scalar setting.

## What the call actually does `ResponseSpecBuilder.addResponseSpecification(ResponseSpecification other)` reads like an append, and the `add` prefix encourages that reading. What it does is run REST Assured's internal specification merge with **the builder's own spec as the target and `other` as the source**. The merge has two behaviours, and the surprising one is that every scalar setting is *assigned* from the source — not compared, not defaulted, and not skipped when the source never set it. ## The merge, field by field | Setting | What the merge does | |---|---| | Status code | assigned from the incoming spec | | Status line | assigned from the incoming spec | | Content type | assigned from the incoming spec | | Expected response time | assigned from the incoming spec | | Response log detail | assigned from the incoming spec | | Fallback (default) parser | assigned from the incoming spec | | Body root path | assigned from the incoming spec | | Body matchers | appended to what the builder holds | | Header assertions | appended | | Cookie assertions | appended | | Registered content-type parsers | added to the registrar | ## Why an unset field is the destructive case - A fresh `ResponseSpecBuilder` starts with no status code, no status line, no content type and no response-time expectation — all of those fields are null. - A spec built only to carry `expectBody("quotaRemainingKg", greaterThan(0))` therefore still has a null status code when you hand it to the merge. - The merge assigns that null straight over the `expectStatusCode(200)` you set on the builder a line earlier. It is an assignment, not a fill-if-absent. - Nothing fails. The spec is still valid, still attaches to calls, and still passes — it simply no longer checks the status. A check that has stopped running is indistinguishable from a check that passes, which is why this survives review. ## Diagnosing it 1. Reproduce the loss in isolation: build the composed spec and validate it against a fishing-quota response you know has the wrong status. If that passes, the expectation is gone. 2. Find `addResponseSpecification` in the chain and note everything that comes **before** it. Those are the settings at risk. 3. Read what the argument spec declares. If it is silent about a scalar, the merge writes null; if it declares one, it wins outright. Either way the builder's earlier value does not survive. 4. Do not expect to read the spec back. There is no querier for a response specification — `SpecificationQuerier.query(...)` accepts a `RequestSpecification` only — so behaviour is the only instrument you have. ## Composing specs safely - **Merge first, then layer.** `new ResponseSpecBuilder().addResponseSpecification(shared).expectStatusCode(201)...` puts the merge on an empty builder, so nothing of yours can be overwritten, and your scalars are set afterwards. - **Or re-declare after the merge.** Call order decides here, exactly as it does elsewhere in this library; a scalar set after the merge line survives it. - **Give one layer the right to own each scalar.** A shared spec that owns the content type and a leaf spec that owns the status code never collide, whatever the order. - **Prefer one flat spec per service** to a tower of merged ones. Composition buys little here and costs you a whole class of silent failure. ## The argument must be REST Assured's own type The method casts the argument to the library's internal response-specification implementation and throws `IllegalArgumentException` if it is anything else, so a `ResponseSpecification` you wrote yourself is rejected outright. The message is not much help — an implementation slip makes it read `specification must be of type class java.lang.Class` — so read it as "not a specification this library built", and pass one that came from `ResponseSpecBuilder.build()` or from `expect()`. ## What this is not This is not the same mechanism as attaching a finished specification to a call. `addResponseSpecification` folds one specification into another **at build time**, before any request exists and before there is a response to look at. Whether an attached specification's checks are merged into a validation chain or evaluated on their own is a separate question with a separate answer, and confusing the two is how people conclude that the merge "sometimes" drops expectations. It does not: it drops them every time the incoming spec is silent about the field, and the only variable is which spec you handed to which side.

  • What happens if you pass addResponseSpecification a ResponseSpecification you implemented yourself?
    It throws `IllegalArgumentException` before merging anything. The method casts the argument to REST Assured's internal response-specification implementation and rejects everything else. The message is unhelpful — an implementation slip makes it read `must be of type class java.lang.Class` — so read it as "not a spec this library built" and pass one from `ResponseSpecBuilder.build()` or `expect()`.
  • Which parts of the response-side merge add rather than overwrite?
    Body matchers, header assertions, cookie assertions and content-type-to-parser registrations are appended. Everything else — status code, status line, content type, expected response time, response log detail, fallback parser and body root path — is assigned from the incoming specification. So composing specs is safe for the plural checks and destructive for the singular ones.

saying these in an interview costs you the question

  • Thinks addResponseSpecification only adds and never overwrites
  • Assumes an unset field in the incoming spec is skipped
  • Believes the builder's own settings take precedence
  • Says call order does not matter for the merge
  • Expects a failure when two specs disagree on status