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

Integration testing — tests/, in-process routers & testcontainers

# The tests/ directory model, driving axum routers in-process with tower::ServiceExt, #[sqlx::test] databases, testcontainers-rs, wiremock for HTTP stubs, and async test mechanics.

Conceptsaved 2026-08-09 #rust#testing#integration#testcontainers#sqlx

Overview

Integration tests in Rust have a dedicated compilation model: every file in tests/ is built as its own crate linking your library like an external consumer — public API only, by construction. Above that sit three workhorse techniques: driving the HTTP stack in-process (a tower Router is callable without a socket), running real dependencies in containers (testcontainers-rs, or sqlx's test-database macro), and stubbing third-party HTTP with wiremock. Async is the main mechanical difference from the neighbours: tests are #[tokio::test] functions and the usual async hazards (leaked tasks, timeouts) apply.

Key points

  • tests/ files are separate crates: public-API-only testing by construction, and each file links the library independently — group scenarios in fewer files (or a tests/it/ module tree) to keep link times sane; shared helpers live in tests/common/mod.rs or a dev-dependency crate.
  • #[tokio::test] wraps a test in a runtime; multi-thread flavor (#[tokio::test(flavor = "multi_thread")]) when the code under test spawns; tokio::time::pause() fast-forwards timers — retry/timeout logic tests run in microseconds.
  • In-process HTTP: router.oneshot(Request::builder()...) (tower::ServiceExt) drives the full axum stack — routing, extractors, middleware — with no port, no flakiness; actix's test::init_service is the same idea. Reserve a real TcpListener + reqwest for the smoke layer.
  • #[sqlx::test] is the relational sweet spot: creates a fresh database per test, runs migrations, hands the pool in as an argument, cleans up after — parallel-safe database tests with zero boilerplate (needs a running server; pair with a compose/CI service).
  • testcontainers-rs (testcontainers + testcontainers-modules) starts Postgres/Redis/Kafka/anything per test or shared per binary — same substrate as the Java and Go stacks; the module catalog is smaller than Java's but covers the staples.
  • wiremock (wiremock-rs) stubs outbound HTTP: mount matchers on a local MockServer, point the client's base URL at it, assert on received requests — the reqwest-era answer to Java's WireMock.
  • Segregation: #[ignore] on the container-heavy shelf plus cargo nextest run -E 'not test(/slow/)' filtersets, or a tests/e2e_* naming convention — keep the default cargo test fast.

Examples

#[tokio::test]
async fn creates_a_user() {
    let app = build_router(test_state());
    let res = app
        .oneshot(Request::post("/users")
            .header("content-type", "application/json")
            .body(Body::from(r#"{"name":"ada"}"#)).unwrap())
        .await.unwrap();
    assert_eq!(res.status(), StatusCode::CREATED);
}

#[sqlx::test(migrations = "./migrations")]
async fn persists_and_reads(pool: PgPool) -> sqlx::Result<()> {
    users::insert(&pool, "ada").await?;
    assert_eq!(users::count(&pool).await?, 1);
    Ok(())
}

Related