Overview
End-to-end means the deployed system exercised through its public surface — HTTP API, browser UI, or CLI binary. Rust's E2E story is assembled rather than packaged: reqwest plus the ordinary test harness for API suites, WebDriver crates for the browser (the polished Playwright/Selenium ecosystems live outside Rust and are often borrowed wholesale), and — where Rust genuinely shines — first-class tooling for testing binaries, because so many Rust deliverables are CLIs.
Key points
- API E2E is reqwest + the harness: an
#[ignore]-tagged suite hitting a staging URL from env config — same shape as the in-process tests, one boundary further out; keep it a thin smoke layer over the integration tier. - Spawn-the-binary tests:
assert_cmdruns your compiled binary with args/stdin and asserts on status/stdout (predicatesfor matchers);instasnapshots the output;escargotpicks specific build profiles. This is the house specialty — the reason Rust CLIs ship with real E2E coverage. - Browser automation: thirtyfour (WebDriver/Selenium protocol) is the maintained Rust-native route; chromiumoxide drives headless Chrome over CDP. Pragmatic teams run Playwright (TS) against Rust backends — the test language needn't match the service language, same conclusion as Go's E2E note.
- Contract testing is the gap: pact-rust exists (pact_consumer / pact_verifier_cli) but the ecosystem is a step behind JVM/Go maturity; teams more commonly pin OpenAPI schemas (utoipa-generated) and diff them in CI, or verify tonic protos with buf breaking-change checks — schema-first gRPC (tonic) makes the contract explicit by construction.
- Environment discipline transfers unchanged: E2E owns deployment confidence, not correctness; deterministic seeds, isolated tenants, and the pyramid's thin top apply as everywhere — see Testing strategies.
Examples
// tests/e2e_smoke.rs — runs with: cargo test -- --ignored
#[tokio::test]
#[ignore = "hits staging"]
async fn health_and_crud_roundtrip() {
let base = std::env::var("E2E_BASE_URL").unwrap();
let client = reqwest::Client::new();
assert!(client.get(format!("{base}/healthz")).send().await.unwrap().status().is_success());
}
// CLI E2E with assert_cmd
#[test]
fn prints_version() {
assert_cmd::Command::cargo_bin("mytool").unwrap()
.arg("--version")
.assert()
.success()
.stdout(predicates::str::starts_with("mytool "));
}
Related
- Testing in Rust — strategies & tooling map — parent map.
- Integration testing — the tier that should carry most of the load.
- REST in Rust — the OpenAPI schemas that substitute for contract tooling.
- End-to-end testing in Java and in Go — richer browser/contract ecosystems, same strategy.