Testing Multipart Upload Requests in Axum: Three Approaches

Multipart uploads are often essential when building APIs for file uploads or complex form submissions. Testing them thoroughly is just as important as implementing them. This article provides an overview of three distinct testing approaches for multipart requests in Axum, taken from my demo repository. I will walk through the structure, sample code, and the benefits and drawbacks of each approach. Introduction: How Axum Handles Tests Axum itself demonstrates various testing patterns in its official repository. In that example, we see how to: Use method like tower::ServiceExt to send requests without starting a real server. Use request client like reqwest to send requests with HTTP server. These official examples confirm that Axum offers a flexible testing environment. Inspired by those patterns, this article explores three ways to test file uploads (multipart requests) specifically. My goal is to provide you with multiple strategies, each with varying levels of realism and complexity. Testing Approaches It includes: Oneshot approach (lightweight, no full server). Axum Test (using specialized test crates). HTTP Server with Reqwest (fully simulates real requests). Each approach has its own folder (and code sample) in the examples/ directory. 1. Oneshot Testing The Oneshot approach uses tower::ServiceExt::oneshot. It allows you to: Test your Axum service without HTTP server. Below is a simplified version of the oneshot test (examples/oneshot.rs): #[cfg(test)] mod tests { use super::*; use axum::http::Request; use axum::body::Body as AxumBody; use tower::ServiceExt; // for oneshot use common_multipart_rfc7578::client::multipart::{Form as MultipartForm, Body as MultipartBody}; use http_body_util::BodyExt; #[tokio::test] async fn test_with_oneshot() -> anyhow::Result { // Prepare Axum app let app = create_app(); // Create a multipart form let mut form = MultipartForm::default(); form.add_text("file", "hoge-able"); form.add_file("csv", "./dummy/test_upload.csv")?; // Create request let content_type = form.content_type(); let body = MultipartBody::from(form); let req = Request::builder() .method("POST") .uri("/upload") .header("Content-Type", content_type) .body(AxumBody::from_stream(body))?; // Send request using oneshot let response = app.oneshot(req).await?; assert_eq!(response.status(), 200); // Check response body let response_body = response.into_body().collect().await?.to_bytes().to_vec(); assert_eq!( String::from_utf8(response_body)?, "Files uploaded successfully" ); Ok(()) } } In fact, there is no definition that successfully converts multipart structures of crates such as reqwest into http::Body. Therefore, it is very easy to convert multipart structures to httt::Body if there is a structure that defines multipart structures and can convert them directly to http::Body (implementing into_stream()). Benefits Fast: No need to bind to a real port or run a live server. Isolated: Tests focus on your application logic rather than network overhead or environment variables. Simple: Minimal setup, straightforward assertion on requests/responses. 2. Axum Test The Axum Test approach uses the axum-test crate. This library is specifically designed to help test Axum applications: You can use TestServer with a mock transport, removing the need for a real network connection. It provides an intuitive API for building requests, handling multipart data, and asserting responses. Here’s a short look at examples/axum_test_example.rs: #[cfg(test)] mod tests { use super::*; use anyhow::Result; use axum_test::multipart::{MultipartForm, Part}; use axum_test::TestServer; use std::fs; #[tokio::test] async fn test_with_axum_test() -> Result { // Prepare Axum app let app = create_app(); let server = TestServer::builder().mock_transport().build(app)?; // Create multipart form let file = fs::read("./dummy/test_upload.csv")?; let part = Part::bytes(file) .file_name("test_upload.csv") .mime_type("text/csv"); let form = MultipartForm::new() .add_text("file", "hoge-able") .add_part("csv", part); // Send the request let response = server.post("/upload").multipart(form).await; // Assert response.assert_status_ok(); response.assert_text("Files uploaded successfully"); Ok(()) } } Benefits Axum-Specific: Tailored for Axum, so it feels very natural to write these tests. Powerful Assertions: Includes methods like assert_text, making response checks more expressive. No External Server: Uses an int

Apr 14, 2025 - 12:42
 0
Testing Multipart Upload Requests in Axum: Three Approaches

Multipart uploads are often essential when building APIs for file uploads or complex form submissions. Testing them thoroughly is just as important as implementing them. This article provides an overview of three distinct testing approaches for multipart requests in Axum, taken from my demo repository. I will walk through the structure, sample code, and the benefits and drawbacks of each approach.

Introduction: How Axum Handles Tests

Axum itself demonstrates various testing patterns in its official repository. In that example, we see how to:

  • Use method like tower::ServiceExt to send requests without starting a real server.
  • Use request client like reqwest to send requests with HTTP server.

These official examples confirm that Axum offers a flexible testing environment. Inspired by those patterns, this article explores three ways to test file uploads (multipart requests) specifically. My goal is to provide you with multiple strategies, each with varying levels of realism and complexity.

Testing Approaches

It includes:

  • Oneshot approach (lightweight, no full server).
  • Axum Test (using specialized test crates).
  • HTTP Server with Reqwest (fully simulates real requests).

Each approach has its own folder (and code sample) in the examples/ directory.

1. Oneshot Testing

The Oneshot approach uses tower::ServiceExt::oneshot. It allows you to:

  • Test your Axum service without HTTP server.

Below is a simplified version of the oneshot test (examples/oneshot.rs):

#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::Request;
    use axum::body::Body as AxumBody;
    use tower::ServiceExt; // for oneshot
    use common_multipart_rfc7578::client::multipart::{Form as MultipartForm, Body as MultipartBody};
    use http_body_util::BodyExt;

    #[tokio::test]
    async fn test_with_oneshot() -> anyhow::Result<()> {
        // Prepare Axum app
        let app = create_app();

        // Create a multipart form
        let mut form = MultipartForm::default();
        form.add_text("file", "hoge-able");
        form.add_file("csv", "./dummy/test_upload.csv")?;

        // Create request
        let content_type = form.content_type();
        let body = MultipartBody::from(form);
        let req = Request::builder()
            .method("POST")
            .uri("/upload")
            .header("Content-Type", content_type)
            .body(AxumBody::from_stream(body))?;

        // Send request using oneshot
        let response = app.oneshot(req).await?;
        assert_eq!(response.status(), 200);

        // Check response body
        let response_body = response.into_body().collect().await?.to_bytes().to_vec();
        assert_eq!(
            String::from_utf8(response_body)?,
            "Files uploaded successfully"
        );

        Ok(())
    }
}

In fact, there is no definition that successfully converts multipart structures of crates such as reqwest into http::Body.
Therefore, it is very easy to convert multipart structures to httt::Body if there is a structure that defines multipart structures and can convert them directly to http::Body (implementing into_stream()).

Benefits

  • Fast: No need to bind to a real port or run a live server.
  • Isolated: Tests focus on your application logic rather than network overhead or environment variables.
  • Simple: Minimal setup, straightforward assertion on requests/responses.

2. Axum Test

The Axum Test approach uses the axum-test crate. This library is specifically designed to help test Axum applications:

  • You can use TestServer with a mock transport, removing the need for a real network connection.
  • It provides an intuitive API for building requests, handling multipart data, and asserting responses.

Here’s a short look at examples/axum_test_example.rs:

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Result;
    use axum_test::multipart::{MultipartForm, Part};
    use axum_test::TestServer;
    use std::fs;

    #[tokio::test]
    async fn test_with_axum_test() -> Result<()> {
        // Prepare Axum app
        let app = create_app();
        let server = TestServer::builder().mock_transport().build(app)?;

        // Create multipart form
        let file = fs::read("./dummy/test_upload.csv")?;
        let part = Part::bytes(file)
            .file_name("test_upload.csv")
            .mime_type("text/csv");
        let form = MultipartForm::new()
            .add_text("file", "hoge-able")
            .add_part("csv", part);

        // Send the request
        let response = server.post("/upload").multipart(form).await;

        // Assert
        response.assert_status_ok();
        response.assert_text("Files uploaded successfully");

        Ok(())
    }
}

Benefits

  • Axum-Specific: Tailored for Axum, so it feels very natural to write these tests.
  • Powerful Assertions: Includes methods like assert_text, making response checks more expressive.
  • No External Server: Uses an internal mock_transport and never binds to a port.

3. HTTP Server with Reqwest

Lastly, there is the full integration test approach. This technique starts an actual Axum server listening on a TCP socket, and then uses Reqwest to send multipart requests:

  • Closely simulates real-world scenarios.
  • Helps you catch issues that only appear in a truly networked environment (e.g. timeouts, actual HTTP headers).
  • Great for end-to-end testing, though slower than the previous methods.

An excerpt from examples/http_server_with_reqwest.rs:

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::{Ok, Result};
    use reqwest::multipart::{Form, Part};
    use reqwest::Client;
    use std::fs;

    #[tokio::test]
    async fn test_with_http_server() -> Result<()> {
        // Prepare Axum app
        let app = create_app();
        let listener = TcpListener::bind("127.0.0.1:3001").await.unwrap();
        let addr = listener.local_addr().unwrap();

        // Spawn the server
        tokio::spawn(async move {
            axum::serve(listener, app).await.unwrap();
        });

        // Create multipart form
        let file = fs::read("./dummy/test_upload.csv")?;
        let part = Part::bytes(file)
            .file_name("test_upload.csv")
            .mime_str("text/csv")?;
        let form = Form::new()
            .text("file", "hoge-able")
            .part("csv", part);

        // Send request using Reqwest
        let response = Client::new()
            .post(&format!("http://{}/upload", addr))
            .multipart(form)
            .send()
            .await?;

        // Assert
        assert_eq!(response.status(), 200);
        assert_eq!(
            response.text().await?,
            "Files uploaded successfully"
        );

        Ok(())
    }
}

Benefits

  • Closest to Production: Involves real network connections, actual HTTP protocols, etc.
  • E2E Scenarios: Useful for testing external dependencies or any networking layers.
  • Catch Subtleties: May discover issues (like CORS, TLS, or routing) that other tests miss.

Conclusion

Testing file uploads in Axum does not have to be overly complicated. Whether you’re optimizing for speed, code simplicity, or realism, these three approaches give you flexibility:

  1. Oneshot: Quick unit-like tests for your Axum routes.
  2. Axum Test: A specialized library for Axum, which makes for concise tests.
  3. HTTP Server: A complete integration approach using a live server and a Reqwest client.

Adopting the right approach—or mixing them together—ensures you have robust coverage. You can keep your tests fast while still ensuring your application behaves correctly when actual users upload files.

Happy testing!