skip to content

In REST Assured, why does a Java then() chain report one failed expectation but the Kotlin Then block all of them?

level: middleimportance: should knowfreq 38%

answer

  1. eager validation, one link at a time
  2. then() already holds the response
  3. the Kotlin block disables eager assert
  4. one forced validation pass at the end
  5. message counts the failed expectations

basics

~20 s

The Java chain validates eagerly: each expectation is checked as it is added, so the first mismatch throws and the rest never run. The kotlin-extensions Then block disables that, collects everything, then validates once, so one AssertionError lists every failure.

solid answer

~40 s

In the Java DSL every expectation method — `statusCode`, `body`, `header`, `contentType`, `time` — routes through the same internal helper. Because `then()` already holds the response, that helper validates immediately, so `then().statusCode(200).body("state", equalTo("SPINNING"))` throws at the first mismatch and the later links never execute; the `AssertionError` reads `1 expectation failed.`. The `kotlin-extensions` module's `Then { }` wraps the block differently: it calls `forceDisableEagerAssert()` first, runs your expectations so they are only registered, then calls `forceValidateResponse()`. Everything is checked in one pass, and a single `AssertionError` lists every mismatch — `3 expectations failed.` with three blocks under it. The project's Kotlin documentation names this as the module's headline benefit. The handoff to the runner is identical either way; only the amount of information in the message differs.

code

kotlin · 17 lines
kotlin
import io.restassured.module.kotlin.extensions.Given
import io.restassured.module.kotlin.extensions.Then
import io.restassured.module.kotlin.extensions.When
import org.hamcrest.Matchers.equalTo

// the status matches; both body paths do not
Given {
    baseUri("https://turbines.example")
} When {
    get("/api/turbines/{serial}/status", "WTG-114")
} Then {
    statusCode(200)
    body("state", equalTo("SPINNING"))
    body("rotorRpm", equalTo(14))
}

// java.lang.AssertionError: 2 expectations failed.

go deeper

for a junior

Know the observable difference: a Java chain shows you the first broken expectation, the Kotlin block shows you all of them. Both fail the test the same way.

for a middle

Explain the mechanism. The response is already attached when then() runs, so each expectation validates as it is added; the Kotlin block turns that off and forces one validation pass at the end.

for a senior

Weigh it as a feedback-loop decision for a real suite: how many CI rounds a multi-fault test costs under each style, and whether adopting an extra module is worth that for your team.

for a principal

Frame the choice at portfolio level. Decide whether a mixed Java and Kotlin estate is worth the divergence in failure reporting, and what the team standardises on so triage stays predictable.

## Two validation modes behind one flag REST Assured's internal response specification carries a private `forceDisableEagerAssert` flag and a helper, `isEagerAssert()`, that returns true when that flag is off **and** a response is already attached to the specification. Every expectation-adding method routes through a wrapper, `validateResponseIfRequired`, which does three things in order: 1. If eager mode is on, reset the body matchers, so only the matcher you are about to add is checked rather than every matcher registered so far. 2. Run the closure that actually registers your expectation. 3. If eager mode is on, validate the response immediately. Everything else in this answer follows from those three steps and from who turns the flag on. ## The Java chain is eager When you write `when().get("/api/turbines/{serial}/status", "WTG-114").then()`, the validatable response is constructed with the response already in hand, so `isEagerAssert()` is true from the first link onward. Walking a three-link chain: - `then().statusCode(200)` registers the status expectation and validates it there and then. - `.body("state", equalTo("SPINNING"))` registers that matcher and validates. - If the turbine reports `FEATHERED`, the `AssertionError` is thrown at that link, and `.body("rotorRpm", greaterThan(0))` is never evaluated at all — the method has already unwound. - The message therefore reads `1 expectation failed.`, even though the source listed three expectations and two of them were genuinely wrong. That is not a bug; it is what "validate as you go" means. But it does mean a Java chain gives you one broken thing per run. ## The Kotlin block batches The `kotlin-extensions` module defines `Then` as an infix function over the response. Its body does three things in a fixed order: - `forceDisableEagerAssert()` — turn eager mode off before your expectations run. - `apply(block)` — execute the block, during which each expectation is only *registered*. - `forceValidateResponse()` — read the body once, then validate everything in a single pass. The result is one `AssertionError` whose message reads `3 expectations failed.` with a block per mismatch beneath it. The project's own Kotlin documentation lists "All failed expectations are reported at the same time" as the module's first advantage over the Java API, which is exactly this mechanism described from the outside. ## Side by side | | Java `then()` chain | `kotlin-extensions` `Then { }` | |---|---|---| | when validation runs | after each expectation | once, after the block | | on the first mismatch | throws immediately | records it and keeps going | | typical message count | `1 expectation failed.` | every mismatch, counted | | thrown type | `java.lang.AssertionError` | `java.lang.AssertionError` | | handoff to the runner | identical | identical | ## Why it matters at the handoff The runner never sees any of this machinery — it sees one throwable escaping one method, exactly as it always does. What changes is how much the message tells you, and that is a feedback-loop cost you pay per CI run: - With eager validation, a turbine test that is wrong in three ways takes three runs to fully diagnose: fix, push, discover the next mismatch, repeat. - With batched validation, the first run names all three, and one fix covers them. - The exception type, the exit path and the report format are unchanged, so this is a pure readability win rather than an integration difference. ## Practical notes - The `spring-mock-mvc-kotlin-extensions` and `spring-web-test-client-kotlin-extensions` modules use the same pair of calls around their own `Then { }` blocks, so they batch identically. The behaviour comes from the shared flag, not from anything specific to HTTP. - A Java chain can still report more than one failure when the expectations arrive together rather than link by link — validating a prepared response specification checks all of its expectations in one pass. - Nothing here changes extraction. `forceValidateResponse()` deliberately reads the body as a string before validating, precisely so `Extract { }` still works after the validation has run. - Adopting `kotlin-extensions` is an artifact decision, not a rewrite: it is a separate `io.rest-assured` coordinate that depends on `rest-assured`, so adding it brings the DSL along and leaves your existing Java tests untouched. - Neither style changes what the runner records. Whichever one you pick, the test fails because a `java.lang.AssertionError` escaped the method, so exit codes, report formats and CI behaviour are untouched by the choice.

  • Do the Spring modules' Kotlin extensions behave the same way?
    Yes. `spring-mock-mvc-kotlin-extensions` and `spring-web-test-client-kotlin-extensions` wrap their `Then { }` blocks with the same disable-then-force-validate pair, so they batch expectations exactly as `kotlin-extensions` does. The behaviour lives in the shared response specification, not in anything specific to HTTP or to a particular entry point.
  • Can a Java then() chain ever report more than one failed expectation?
    Yes. The count is simply how many registered expectations mismatched in one validation pass, and validating a prepared response specification checks all of its expectations together. What the chain cannot do is continue after it throws, which is why link-by-link failures arrive one per run.

saying these in an interview costs you the question

  • Thinks the Java chain collects all expectations before validating anything
  • Believes the Kotlin block is only syntax sugar for escaping the when keyword
  • Assumes the Kotlin module makes a different exception type reach the runner
  • Says the chain stops at the first failure because Hamcrest short-circuits
  • Expects then().body(...).body(...) to report both mismatches in one run
  • Thinks batching requires giving up extraction after validation