← Week 3: Axum Web Framework

Day 20: Testing Axum Services

Phase 2 · Jun 29, 2026

← Week 3: Axum Web Framework

Agenda (2–3 hours)

  • Read (45 min): axum::test_helpers / axum-test crate docs; tower::ServiceExt documentation
  • Study (45 min): What's the difference between unit-testing a handler, integration-testing via TestClient, and end-to-end testing against a running server?
  • Practice (45 min): Write tests for create/read/update/delete user endpoints using both TestClient and tower::ServiceExt::oneshot
  • Challenge (30 min): Test a middleware (auth layer) in isolation using tower_test::mock::Mock
← Week 3: Axum Web Framework

Testing without a Network

Axum's Router implements Service<Request> — you can call it directly:

use tower::ServiceExt; // for .oneshot()
use axum::body::Body;
use http::{Request, StatusCode};

#[tokio::test]
async fn test_get_user() {
    let app = create_app(test_state());

    let response = app
        .oneshot(
            Request::builder()
                .uri("/users/42")
                .header("authorization", "Bearer test-token")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);
}
← Week 3: Axum Web Framework

axum-test / TestClient

use axum_test::TestServer;

#[tokio::test]
async fn test_create_and_read() {
    let app = create_app(test_state());
    let server = TestServer::new(app).unwrap();

    // POST /users
    let res = server.post("/users")
        .json(&json!({ "name": "Alice", "email": "alice@example.com" }))
        .await;
    assert_eq!(res.status_code(), StatusCode::CREATED);
    let user: User = res.json();

    // GET /users/:id
    let res = server.get(&format!("/users/{}", user.id)).await;
    assert_eq!(res.status_code(), StatusCode::OK);
    assert_eq!(res.json::<User>().name, "Alice");
}
← Week 3: Axum Web Framework

Test State

Avoid real databases in unit tests — use an in-memory or mock implementation:

fn test_state() -> AppState {
    AppState {
        db: Arc::new(InMemoryDb::new()),
        jwt: Arc::new(JwtValidator::new_test()),
    }
}

Define a trait UserRepository and implement it with both SqlxPool (production) and InMemoryDb (test). This is the repository pattern.

← Week 3: Axum Web Framework

Testing Middleware in Isolation

#[tokio::test]
async fn auth_middleware_rejects_missing_token() {
    let service = ServiceBuilder::new()
        .layer(AuthLayer::new(test_jwt_config()))
        .service(tower::service_fn(|_req| async {
            Ok::<_, Infallible>(Response::new(Body::from("ok")))
        }));

    let response = service
        .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
← Week 3: Axum Web Framework

Key Takeaways

  • ServiceExt::oneshot lets you test any Axum/Tower service without a running server
  • axum-test provides a higher-level API with JSON helpers and header utilities
  • Use the repository pattern to swap real vs test backends
  • Test middleware in isolation with service_fn as the inner service

Tomorrow: Challenge — REST API with JWT authentication middleware.