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.
-
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
-
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.
-
Creating a Test Module:
In
src/lib.rs
, add the following code to create a test module forhello_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 callinghello_world
matches the expected value.
-
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.
-
Set Up Integration Test:
In the root of the project, create a new folder called
tests
. Under this folder, add a new fileapi_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 inlib.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.
- We create a simple Axum app inside the
-
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
, andtokio
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:
- Explore more advanced test setups, such as using mock services.
- Learn how to test more complex Axum applications with databases and authentication.
Happy testing!