Overview
End-to-end tests exercise the deployed system through its public surface — HTTP API or browser UI — with real infrastructure underneath. They give the highest-fidelity signal and the worst economics: slow, environment-dependent, and the natural home of flakiness. The discipline is to keep this layer thin and deliberate, push checks down to integration tests where possible, and replace cross-service E2E with consumer-driven contracts.
Key points
- REST Assured for API-level E2E: fluent given/when/then DSL over HTTP, JSON/XML path assertions, auth support; the standard for black-box API tests (and bundled as the test convention in Quarkus).
- Playwright over Selenium for new browser suites: auto-waiting, network interception, trace viewer, and bundled browsers cut the classic flake sources; Selenium WebDriver remains the W3C-standard incumbent with the broadest grid/browser ecosystem.
- Karate combines API testing, mocks and load in a Gherkin-like DSL — tests are plain text, no Java glue per step; attractive for mixed-skill teams, opinionated by design.
- Contract testing replaces most cross-service E2E: Pact (consumer-driven) verifies each consumer's expectations against the provider in its own build — no shared environment, failures name the exact broken interaction.
- Spring Cloud Contract is the provider-driven variant for Spring shops: contracts in the provider repo generate provider tests and consumer stubs (via Stubrunner).
- Flakiness is a defect, not weather: quarantine-and-fix, never blind
@RepeatedTestretries; forbidThread.sleepin favour of explicit waits/polling (Awaitility). - A failing E2E test should page someone or block the release — if a failure can be shrugged off, the test shouldn't exist at this layer.
Details
What belongs at this layer
- A handful of user-critical journeys (sign-up, checkout, the money path).
- Smoke tests post-deployment: is the system up, wired and answering.
- Everything else — validation rules, error mapping, edge cases — belongs in unit or integration tests where it is cheap and deterministic.
Contract testing in one pass
- Consumer writes a Pact test against a mock provider → generates a pact file.
- Pact is published to a Pact Broker (versioned, tagged per branch/env).
- Provider's build replays every consumer pact against the real provider
(
@Provider/@PactBrokerJUnit 5 support) and publishes verification results. can-i-deploygates releases on matrix compatibility.
Spring Cloud Contract inverts authorship (contracts live provider-side, in Groovy/YAML) and hands consumers generated WireMock stubs. Both beat a shared staging environment: verification is per-build, isolated and attributable.
E2E in CI
| Approach | How | Trade-off |
|---|---|---|
| Docker Compose in the pipeline | docker compose up app + deps, run suite against localhost |
Simple, portable; drifts from prod topology |
| Testcontainers-orchestrated | Compose module or containers from the test JVM | One tool for integration and E2E; same drift caveat |
| Ephemeral environment | Spin a namespace per PR (k8s, preview envs), deploy real manifests | Highest fidelity; needs platform investment |
Keep E2E suites in a separate CI stage with its own budget (e.g. under 15 minutes), parallelised, producing artefacts on failure (screenshots, Playwright traces, HTTP logs).
Examples
@Test
void createdOrderIsRetrievable() {
String id =
given()
.contentType(ContentType.JSON)
.body(new CreateOrder("widget", 3))
.when()
.post("/orders")
.then()
.statusCode(201)
.extract().path("id");
when()
.get("/orders/{id}", id)
.then()
.statusCode(200)
.body("items[0].sku", equalTo("widget"),
"items[0].quantity", equalTo(3));
}
Related
- Testing in Java — strategies & tooling map — parent map of the strategy portfolio.
- Integration testing — the cheaper layer most checks should be pushed down to.
- REST — the API style REST Assured and contract tests exercise.