How to Test Axum APIs: Unit and Integration Testing Guide

When building web APIs in Rust, Axum has become a popular choice for creating fast, type-safe, and efficient web applications. But how do we ensure that our Axum APIs are working as expected? The answer lies in writing unit tests and integration tests.

In this tutorial, we’ll walk through testing Axum APIs, starting with basic testing concepts and gradually building out a test suite. We’ll use reqwest for making HTTP requests in our integration tests, making the tests more intuitive and flexible. Whether you’re new to testing in Rust or familiar with it, this guide will give you hands-on experience with testing an Axum API, covering both unit and integration tests.

Let’s dive in!


Step 1: Setting up Axum

Let’s begin by creating a simple Axum API. This will serve as the foundation for our tests.

  1. Create a new Rust project:

    If you haven’t already, create a new Rust project:

    cargo new axum_api_tests --bin
    cd axum_api_tests
    
  2. Add Axum and reqwest to Cargo.toml:

    Open Cargo.toml and add Axum, reqwest, and other necessary dependencies:

    [dependencies]
    axum = "0.5"
    tokio = { version = "1", features = ["full"] }
    
    [dev-dependencies]
    reqwest = "0.11"
    
    • axum is the web framework we're using.
    • tokio is required for async tasks.
    • reqwest is the HTTP client library we’ll use for our integration tests.

Step 2: Setting Up the Axum App

To begin, we will create a minimal Axum app with a simple "Hello, World!" route. This app will be the core of our integration and unit tests.

Create two files:

  • src/lib.rs: This will contain the app logic that we want to test.
  • src/main.rs: This will be used to run the app.

lib.rs (The App Logic)

In src/lib.rs, define the Axum app and the hello_world handler:

use axum::{routing::get, Router};

async fn hello_world() -> &'static str {
    "Hello, World!"
}

pub fn app() -> Router {
    Router::new().route("/", get(hello_world))
}

This is a simple route that returns "Hello, World!" when accessed.

main.rs (Running the App)

In src/main.rs, we will set up the server to run the app:

use axum::Server;
use std::net::SocketAddr;
use axum_api_tests::app;  // Import the app from lib.rs

#[tokio::main]
async fn main() {
    let app = app();

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Listening on {}", addr);
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Here, we are importing the app function from lib.rs and using it to start the Axum server on localhost:3000.


Step 3: Unit Testing with Axum

Now, let’s move on to unit testing. Unit tests are isolated tests that check individual components of your code. We'll start by testing the hello_world handler function.

  1. Creating a Test Module:

    In src/lib.rs, add the following code to create a test module for hello_world:

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_hello_world() {
        let response = hello_world().await;
        assert_eq!(response, "Hello, World!");
    }
}
  • #[cfg(test)]: This annotation tells Rust to include the module only when running tests.
  • #[tokio::test]: This macro allows us to write async tests using Tokio.
  • assert_eq!: This macro checks that the result of calling hello_world matches the expected value.
  1. Running the Test:

    To run the test, execute:

    cargo test
    

    You should see the test pass, confirming that the hello_world handler works as expected.


Step 4: Integration Testing with reqwest

Unit tests test isolated units of code, but what if we want to test our API as a whole? This is where integration tests come in. They allow us to simulate real requests to our API and ensure everything works together. In this case, we’ll use reqwest to send HTTP requests to our Axum API.

  1. Set Up Integration Test:

    In the root of the project, create a new folder called tests. Under this folder, add a new file api_tests.rs for integration testing:

    use axum::http::StatusCode;
    use reqwest::Client;
    use std::net::SocketAddr;
    use axum_api_tests::app; // Import the app from lib.rs
    
    #[tokio::test]
    async fn test_hello_world_integration() {
        // Set up the app
        let app = app();
    
        // Start the server in a background task
        let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
        let server = axum::Server::bind(&addr)
            .serve(app.into_make_service());
        tokio::spawn(server);
    
        // Use reqwest to send a GET request to the server
        let client = Client::new();
        let res = client.get("http://localhost:3000/")
            .send()
            .await
            .unwrap();
    
        // Check if the response is as expected
        assert_eq!(res.status(), StatusCode::OK);
        let body = res.text().await.unwrap();
        assert_eq!(body, "Hello, World!");
    }
    

    In this test:

    • We create a simple Axum app inside the app function in lib.rs.
    • We use reqwest::Client to send an HTTP GET request to the server.
    • The server runs in the background using tokio::spawn, and we make a request to ensure the response is correct.
  2. Running the Integration Test:

    Once the integration test is set up, run it with:

    cargo test --test api_tests
    

    If everything is working, you should see the integration test pass, confirming that the whole API behaves as expected.


Step 5: Challenges for You

Now it’s time for you to practice! Try adding new routes to the application and testing them in both unit and integration tests. For example:

  • Add a /goodbye route that returns "Goodbye, World!".
  • Write a unit test for the new handler.
  • Write an integration test to verify the new route.

Recap and Conclusion

In this tutorial, we’ve covered:

  • Unit testing for individual functions in Axum using #[tokio::test].
  • Integration testing where we use reqwest to send HTTP requests to simulate real interactions with the API.
  • How to write tests in Rust using the axum, reqwest, and tokio libraries.

Testing is a crucial part of ensuring the reliability of your web applications. Axum’s simplicity and integration with Rust’s powerful testing tools make it easy to write effective tests for your API.

Next steps:

Happy testing!

Read more