skip to content

Artifacts and Runner Handoff

Which io.rest-assured coordinate a project actually depends on and what each one adds, and how a failed matcher's AssertionError reaches JUnit 5 or TestNG - the library is not a runner.

part ofREST Assuredoverview, primer and where to startread it →
on this pageshow

questions

4

When a REST Assured expectation fails, what is thrown and how does JUnit 5 or TestNG see it?

level: middleimportance: must knowfreq 66%

answer

  1. one AssertionError, not a runner callback
  2. message opens with an expectation count
  3. nothing catches it; it just unwinds
  4. plain java.lang.AssertionError, no opentest4j fields
  5. a Groovy bridge unwraps the wrapper first

basics

~20 s

REST Assured throws a plain java.lang.AssertionError whose message opens with a count of failed expectations and lists each mismatch. Nothing catches it, so it unwinds out of the test method and the runner that called that method records a failure.

solid answer

~40 s

A failed expectation in `then()` ends in REST Assured's internal response specification, which collects the mismatches and throws one `java.lang.AssertionError`. Its message is a count — `1 expectation failed.` or `3 expectations failed.` — followed by a block per mismatch, such as `JSON path state doesn't match.` with expected and actual values beneath it. The library does nothing else: no `try`/`catch` around your test, no reporting, and its main sources reference neither JUnit nor TestNG. The error simply propagates out of the method the runner invoked, and the runner records whatever escaped. Because it is `java.lang.AssertionError` and not an opentest4j `AssertionFailedError`, there are no structured expected/actual fields for tooling to render — the message string is everything. Transport and object-mapping problems escape as their own exception types instead.

code

java · 16 lines
java
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;

// no try/catch, no runner API - the AssertionError just leaves the method
given()
    .baseUri("https://turbines.example")
.when()
    .get("/api/turbines/{serial}/status", "WTG-114")
.then()
    .statusCode(200)
    .body("state", equalTo("SPINNING"));

// java.lang.AssertionError: 1 expectation failed.
// JSON path state doesn't match.
// Expected: SPINNING
//   Actual: FEATHERED

go deeper

for a junior

Remember the shape of the failure: a java.lang.AssertionError whose message begins with a count of failed expectations. You do not add any assert call of your own around a then() chain.

for a middle

Explain the mechanism end to end - the internal response specification collects mismatches, throws one AssertionError, and nothing in the library catches it on the way out to the runner.

for a senior

Demonstrate triage. Separate an AssertionError, which means the assertions ran and disagreed, from an IllegalStateException or a transport exception, which means the classpath or environment is wrong.

for a principal

Argue the tradeoff of runner independence: portability across JUnit and TestNG bought at the cost of structured expected/actual data, and what the team invests in logging or reporting to compensate.

## Where the AssertionError is born Every expectation you attach in `then()` — a status code, a status line, a header, a cookie, a content type, a response time, a body path — is recorded on REST Assured's internal response specification and then checked against the response. When the checks produce mismatches, that class does exactly one thing: ```groovy throw new AssertionError("$numberOfErrors expectation$s failed.\n$errorMessage$formattedOnFailMessage") ``` That is `java.lang.AssertionError`, the JDK's own type, constructed with a message string and nothing else. There is no REST Assured exception hierarchy for validation, no error code, no structured payload and no callback. ## What the message contains The message has a fixed shape, and reading it at a glance is a real skill on a red build: - It opens with a count and a pluralised noun: `1 expectation failed.` or `3 expectations failed.` - Under that comes one block per mismatch. A body-path mismatch reads `JSON path <path> doesn't match.` followed by `Expected:` and `Actual:` lines; status code, status line, content type and response time each have their own wording. - An `onFailMessage` you supplied is appended at the very end. A failing turbine assertion therefore reaches CI looking like this: ```text java.lang.AssertionError: 1 expectation failed. JSON path state doesn't match. Expected: SPINNING Actual: FEATHERED ``` ## How the runner finds out: it doesn't, you throw at it There is no integration. REST Assured never registers a listener, never calls a reporting API and never names a runner. Both JUnit 5 and TestNG invoke your test method reflectively and record whichever throwable escapes it; "throw and let it unwind" is the one contract every JVM runner honours, and it is the entire handoff. That is why the library needs no adapter for JUnit 4, JUnit 5, TestNG or anything else: it never learns which of them is running, and it never needs to. - The library has no `@Test`, no lifecycle hooks and no result model of its own. - Its main sources contain no reference to JUnit, TestNG or opentest4j. `junit-jupiter` appears in its build only at test scope, for testing the library itself. - Consequently the same `given()...then()` chain compiles and fails identically under either runner. ## The Groovy boundary, and why you never see it REST Assured's request implementation is written in Groovy, and every verb method — `get`, `post`, `put`, `delete`, `head`, `patch`, `options` — wraps its work in `GroovyAssertBridge.runWithUnwrap`. Groovy and reflective dispatch like to wrap the real cause in `GroovyRuntimeException`, `InvocationTargetException` or `UndeclaredThrowableException`; the bridge unwraps those containers recursively and rethrows the original throwable, using a sneaky throw so the method signature does not change. Without it your CI report would show a Groovy wrapper with the interesting failure buried two `Caused by:` levels down. With it, the `AssertionError` — or the real transport exception — is the top-level throwable the runner records. ## Not everything that fails is an AssertionError | What went wrong | What leaves the library | |---|---| | an expectation did not match | `java.lang.AssertionError` | | no object mapper on the classpath | `IllegalStateException` | | a named object mapper is absent | `IllegalArgumentException` | | the turbine service is unreachable, or TLS fails | the transport's own exception, unwrapped | That distinction is the practical half of the answer. A red build full of `AssertionError` is a build whose assertions ran and disagreed with the service; a red build full of `IllegalStateException` is a build whose classpath or environment is wrong, and no amount of staring at the turbine API will explain it. What a report calls each category afterwards is the runner's and the reporting tool's business, not the library's. ## What this design buys, and what it costs - **Portability.** JUnit 4, JUnit 5, TestNG, Spock or a bare `main` method all work, with no adapter module per runner and no version matrix to maintain. - **Predictability.** One failure produces one throwable, at one point in the chain, with a message you can grep for in a log. - **A real cost.** Because it is a bare `AssertionError` rather than an opentest4j `AssertionFailedError`, there are no machine-readable expected and actual fields for an IDE to render as a side-by-side diff. Everything a tool can show you is inside the message string, which is precisely why REST Assured invests so heavily in request and response logging.

  • Does REST Assured behave differently under TestNG than under JUnit 5?
    No. The library never names either one: its main sources hold no JUnit or TestNG reference, and `junit-jupiter` appears only at test scope in its own build. It throws `java.lang.AssertionError` and stops. Both runners treat an escaping throwable as a failed test, so the same chain works unchanged; the annotations, lifecycle and reporting around it belong to the runner.
  • What does REST Assured throw if the turbine service is unreachable?
    Not an `AssertionError`. The call fails inside the transport and the underlying exception comes out as itself, because `GroovyAssertBridge.runWithUnwrap` strips the Groovy and reflection wrappers so you see the real cause rather than an `InvocationTargetException`. Only a failed expectation produces an `AssertionError`, which is why the two categories are worth telling apart on a red build.
  • Why does the message sometimes say "3 expectations failed" instead of one?
    Because the count is simply how many registered expectations were found to mismatch in that validation pass. A pass that checks several expectations at once — a prepared response specification, or the Kotlin `Then { }` block — can report all of them together, while a plain Java chain validates after each link and throws on the first.

The library behaves like a smoke detector with no wiring to the fire brigade: it makes the noise and stops there. Whoever called the test method - JUnit 5, TestNG, or a plain main - is what decides the noise means a failed test.

saying these in an interview costs you the question

  • Thinks REST Assured reports the result to JUnit through some integration API
  • Expects a runner-specific type such as AssertionFailedError or ComparisonFailure
  • Believes you must wrap then() in an assert or fail() call for the test to fail
  • Assumes a connection refused surfaces as an AssertionError like a matcher mismatch
  • Thinks switching from JUnit 5 to TestNG changes what the library throws
  • Claims REST Assured needs a JUnit dependency present in order to work
open as a page

Which io.rest-assured coordinate does a REST Assured test project depend on, and what comes with it?

level: juniorimportance: should knowfreq 62%

basics

~20 s

The one coordinate io.rest-assured:rest-assured at test scope covers API testing; it brings json-path, xml-path, Groovy, Apache HttpClient and Hamcrest with it. Object mapping, JSON schema validation, Kotlin blocks and Spring support are separate artifacts, and no test runner is included.

open as a page

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%

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.

open as a page

A REST Assured suite dies with NoSuchMethodError on an org.hamcrest class — how do you fix it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Two Hamcrest jars are on the test classpath and the older one is winning. REST Assured needs the modern org.hamcrest:hamcrest artifact; a legacy hamcrest-core or hamcrest-all 1.x jar supplies the same classes. Exclude or pin one Hamcrest and it goes.

open as a page