axum-test

repository·main·Indexed 42 days ago

https://github.com/josephlenton/axum-test

An end-to-end testing library for Axum and Actix Web applications (version 21.1.0). It enables developers to run services via mocked networks or real ports to perform assertions on REST, WebSockets, and JSON responses. Features include JSON shape assertions via expect_json, snapshot testing with Insta, cookie management, and support for multipart/form-data requests.

Tokens
14.8K
Snippets
48
Records
63
Agent score
55%

What's inside axum-test

  1. Quickstart: Run E2E tests with TestServer

    main

    To perform end-to-end testing for an Axum application, use TestServer to run your service with either a mocked network or on a random port. You can then use the server instance to build requests (e.g., .get(), .post()) and assert against the responses.

    use axum::Router;
    use axum::routing::get;
    use axum_test::TestServer;
    
    #[tokio::test]
    async fn it_should_ping_pong() {
        // Build an application with a route.
        let app = Router::new()
            .route(&"/ping", get(|| async { "pong!" }));
    
        // Run the application for testing.
        let server = TestServer::new(app);
    
        // Get the request.
        let response = server
            .get("/ping")
            .await;
    
        // Assert!
        response
            .assert_status_ok()
            .assert_text("pong!");
    }
    use axum::Router;
    use axum::routing::get;
    
    use axum_test::TestServer;
    
    #[tokio::test]
    async fn it_should_ping_pong() {
        // Build an application with a route.
        let app = Router::new()
            .route(&"/ping", get(|| async { "pong!" }));
    
        // Run the application for testing.
        let server = TestServer::new(app);
    
        // Get the request.
        let response = server
            .get("/ping")
            .await;
    
        // Assert!
        response
            .assert_status_ok()
            .assert_text("pong!");
    }
  2. Configure Axum Test features

    main

    Axum Test provides several optional features to extend its capabilities. By default, only pretty-assertions is enabled.

    FeatureDescription
    allTurns on all non-deprecated features.
    pretty-assertionsOn by default. Uses the pretty_assertions crate for better error output.
    actix-webEnables support for using Axum Test with Actix Web.
    yamlEnables support for sending, receiving, and asserting YAML content.
    msgpackEnables support for sending, receiving, and asserting msgpack content.
    typed-routingEnables support for using TypedPath in requests (requires axum-extra).
    wsEnables WebSocket support.
    reqwestEnables TestServer to create Reqwest requests for querying.
  3. Configure TestServer transport modes with the Transport enum

    main

    The Transport enum determines how TestRequest communicates with the TestServer. You can choose between a mock HTTP transport, a real server on a random port, or a real server with specific network configurations.

    Available modes:

    • MockHttp: The default mode. Uses a mock HTTP transport instead of the network stack.
    • HttpRandomPort: Spins up a real web server on a random available port. Requests are made over the actual network stack.
    • HttpIpPort: Spins up a real web server where you can specify the IP address and port.
      • ip: The IP address to bind to (defaults to 127.0.0.1 if None).
      • port: The port number to bind to (defaults to a random port if None).
    • HttpTcpListner: Spins up a real web server using a provided TcpListener instance.
    // Example of using HttpIpPort to bind to a specific address and port
    let transport = Transport::HttpIpPort {
        ip: Some("127.0.0.1".parse().ok()),
        port: Some(8080),
    };
  4. How `Part` works in a `MultipartForm`

    main
    A Part represents an individual section within a multipart form. It encapsulates the raw data (bytes), an optional file_name, a mime_type, and a collection of custom headers. To use it, you typically instantiate it via Part::text() or Part::bytes(), apply any necessary metadata via builder methods, and then register it with a MultipartForm instance using add_part(field_name, part).
  5. How cookies are handled in TestServer

    main

    By default, TestServer does not automatically save and pass cookies back to the server in subsequent requests. You can control this behavior using the TestServerBuilder or per-request methods.

    Global Configuration

    Use TestServer::builder() to set the default cookie behavior for the entire server instance:

    • .save_cookies(): Automatically saves cookies from responses and includes them in subsequent requests.
    • .do_not_save_cookies(): Disables automatic cookie saving (this is the default).

    Per-Request Control

    You can override or specify cookie behavior for individual requests:

    • .save_cookies(): Tells the server to save cookies from this specific response.
    • .clear_cookies(): Clears all currently saved cookies before sending the request.
    • .add_cookie(cookie): Manually adds a specific cookie::Cookie to the request.

    Manual Management

    • server.add_cookie(cookie): Adds a cookie to the server's internal cookie jar for all future requests.
    • server.clear_cookies(): Wipes all cookies currently stored in the server instance.
  6. How typed routing works with TestServer

    main

    If you are using axum-extra's TypedPath, axum-test provides a .typed_get() method to facilitate testing these routes. This allows you to pass a struct that implements TypedPath and automatically handles path parameters and query strings.

    // Example using a typed path struct
    let path = TestingPathQuery { id: 123 }.with_query_params(QueryParams { 
        param: "with-typed-query".to_string(), 
        other: None 
    });
    
    server.typed_get(&path)
        .expect_success()
        .await;

    Key methods for typed requests:

    • .typed_get(&path): Sends a GET request using a typed path.
    • .with_query_params(params): Attaches a struct of query parameters to the typed path.
    • .add_query_param(key, value): Adds an additional query parameter to the request.
    • .clear_query_params(): Removes existing query parameters from the typed path before adding new ones.
  7. Quickstart: Testing an Axum application

    main

    To test an Axum application, create a TestServer by passing your Router to TestServer::new(). You can then make requests using HTTP method helpers (like .put(), .get(), etc.) and assert the results using TestResponse methods.

    use axum::Router;
    use axum::extract::Json;
    use axum::routing::put;
    use axum_test::TestServer;
    use serde_json::json;
    use serde_json::Value;
    
    async fn route_put_user(Json(user): Json<Value>) -> () {}
    
    let app = Router::new().route("/users", put(route_put_user));
    let server = TestServer::new(app);
    
    let response = server.put("/users")
        .json(&json!({
            "username": "Terrance Pencilworth",
        }))
        .await;
    use axum::Router;
    use axum::extract::Json;
    use axum::routing::put;
    use axum_test::TestServer;
    use serde_json::json;
    use serde_json::Value;
    
    async fn route_put_user(Json(user): Json<Value>) -> () {}
    
    let app = Router::new().route("/users", put(route_put_user));
    let server = TestServer::new(app);
    
    let response = server.put("/users")
        .json(&json!({
            "username": "Terrance Pencilworth",
        }))
        .await;
  8. Quickstart: Testing an Actix Web application

    main

    axum-test also supports Actix Web. The usage pattern is identical to Axum: pass an application factory (a closure that returns an App) to TestServer::new().

    use actix_web::{App, HttpResponse, web};
    use serde_json::{Value, json};
    use axum_test::TestServer;
    
    async fn route_put_users(body: web::Json<Value>) -> HttpResponse {
        HttpResponse::Ok().json(json!("done!"))
    }
    
    let new_app = || App::new().route("/json", web::put().to(route_put_users));
    let server = TestServer::new(new_app);
    
    let response = server.put("/users")
        .json(&json!({
            "username": "Terrance Pencilworth",
        }))
        .await;
    use actix_web::{App, HttpResponse, web};
    use serde_json::{Value, json};
    use axum_test::TestServer;
    
    async fn route_put_users(body: web::Json<Value>) -> HttpResponse {
        HttpResponse::Ok().json(json!("done!"))
    }
    
    let new_app = || App::new().route("/json", web::put().to(route_put_users));
    let server = TestServer::new(new_app);
    
    let response = server.put("/users")
        .json(&json!({
            "username": "Terrance Pencilworth",
        }))
        .await;
  9. Initialize a TestServer

    main

    The TestServer runs your Axum or Actix application, allowing you to make HTTP requests against it for testing.

    Quick Start

    For the simplest setup, use TestServer::new(app), where app is an axum::Router or an object implementing IntoMakeService.

    Advanced Configuration

    Use TestServer::builder() to customize the server behavior, such as choosing between mocked HTTP or real networking, enabling cookie saving, or setting default success expectations.

    use axum::Router;
    use axum::routing::get;
    use axum_test::TestServer;
    
    let app = Router::new()
        .route("/hello", get(|| async { "hello!" }));
    
    // Simple initialization
    let server = TestServer::new(app);
    
    // Custom initialization via builder
    let mut server = TestServer::builder()
        .http_transport()
        .expect_success_by_default()
        .save_cookies()
        .build(app);
  10. Use TestRequest to build and send HTTP requests

    main

    A TestRequest is used to construct and execute HTTP requests against a TestServer. You create requests using the builder methods on a TestServer instance (e.g., .get(), .post(), .put(), .delete()). Once the request is configured with headers, bodies, or cookies, you execute it by awaiting the TestRequest object, which returns a TestResponse.

    # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
    # use axum::Router;
    # use axum_test::TestServer;
    #
    # let server = TestServer::new(Router::new());
    #
    // Build your request
    let request = server.get(&"/user")
        .add_header("x-custom-header", "example.com")
        .content_type("application/yaml");
    
    // await request to execute
    let response = request.await;
    #
    # Ok(()) }
  11. Configure and initialize a TestServer using TestServerBuilder

    main

    The TestServerBuilder is the recommended way to create a TestServer instance. It allows you to configure server behavior—such as cookie handling, default content types, and transport layers—before passing your application (e.g., an axum::Router) to the builder.

    Key Methods

    • TestServer::builder(): The entry point for creating a new builder.
    • .build(app): Consumes the builder and the application to return a TestServer. Note: This will panic if the server fails to build.
    • .try_build(app): Returns a Result<TestServer, Error>, allowing for graceful error handling if the server cannot be initialized.
    • .into_config(): Converts the builder into a TestServerConfig object, which can then be used with TestServer::new_with_config(app, config).
    use axum::Router;
    use axum_test::TestServer;
    
    let my_app = Router::new();
    let server = TestServer::builder()
        .save_cookies()
        .default_content_type("application/json")
        .build(my_app);
  12. Test WebSocket Ping Pong with Actix Web

    main

    This example demonstrates how to use axum-test to test WebSocket connections in an Actix Web application. It covers two primary scenarios:

    1. Starting a WebSocket connection: Writing a basic test that successfully establishes a WebSocket handshake.
    2. Ping Pong testing: Verifying bidirectional data flow by pushing data up and down the connection (sending a message and asserting the response).