rgoussu@goussu: ~/library/rust/testing
~/library/rust/testing cat end-to-end-testing.md

End-to-end testing — whole systems & contracts

# E2E in Rust — reqwest smoke suites against deployed services, browser automation with thirtyfour and headless chrome, CLI testing with assert_cmd, and where contract testing stands.

Conceptsaved 2026-08-09 #rust#testing#e2e#contracts#cli

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_cmd runs your compiled binary with args/stdin and asserts on status/stdout (predicates for matchers); insta snapshots the output; escargot picks 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