Testing the API
Integration tests.
Testing the API is a free Learn Rust Coding lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Learn Rust Coding learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Test an API?
Tests give you confidence that endpoints behave correctly and keep working as you change code. For a REST API the most valuable tests are integration tests: they exercise the real router end to end, sending requests and asserting on responses.
This lesson covers unit tests, the Tower oneshot trick, and full integration tests.
Unit Tests for Pure Logic
Logic that does not touch the network, such as validation, can be tested with plain Rust unit tests. Put them in a #[cfg(test)] module beside the code. These run fast with cargo test.
fn validate_title(title: &str) -> bool {
!title.trim().is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty() {
assert!(!validate_title(" "));
assert!(validate_title("buy milk"));
}
}Async Tests
Handlers are async, so test functions that await must run on a runtime. Use #[tokio::test] instead of #[test]. It starts a Tokio runtime for that test, letting you call async code with .await.
async fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn adds() {
assert_eq!(add(2, 3).await, 5);
}
}Testing Without a Network
Axum routers implement Tower's Service trait, so you can feed them requests directly without binding a port. The oneshot method takes a single Request and returns the Response. This makes integration tests fast and deterministic.
// dev-dependencies needed: tower (for ServiceExt), http-body-util
use axum::{Router, routing::get};
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt; // brings in oneshot
async fn check() {
let app = Router::new().route("/health", get(|| async { "ok" }));
let res = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}Asserting on the Status Code
The first thing most tests check is the HTTP status. A missing resource should give 404, a successful create 201, and a bad body 400. Build a request for the route and compare res.status() against the expected code.
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt;
async fn missing_returns_404(app: axum::Router) {
let res = app
.oneshot(Request::builder()
.uri("/todos/999")
.body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}Reading the Response Body
To assert on JSON, collect the response body into bytes and deserialize it. The http_body_util::BodyExt::collect helper gathers the body, then serde_json parses it into your model for field-level assertions.
use http_body_util::BodyExt;
async fn read_json(res: axum::http::Response<axum::body::Body>) {
let bytes = res.into_body().collect().await.unwrap().to_bytes();
let todo: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(todo["done"], false);
}Sending a JSON Body
To test a POST, build a request with a JSON body and the correct content-type header. Serialize your input struct, set content-type: application/json, and pass it through oneshot.
use axum::http::{Request, StatusCode, header};
use axum::body::Body;
use tower::ServiceExt;
async fn create_returns_201(app: axum::Router) {
let body = serde_json::json!({ "title": "test" }).to_string();
let res = app
.oneshot(Request::builder()
.method("POST").uri("/todos")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body)).unwrap())
.await.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
}A Reusable Test App Builder
Each test should start from a clean state. Write a helper that builds a fresh router with a new in-memory or test database. Calling it in every test isolates them so one test cannot affect another.
use axum::Router;
use std::sync::{Arc, Mutex};
fn test_app() -> Router {
let store = Arc::new(Mutex::new(Vec::new()));
build(store) // same build() the real server uses
}The tests Directory
Integration tests live in a top-level tests/ folder. Each file there is compiled as a separate crate that uses your library's public API. This forces you to test through the same interface real users see.
tests/api.rsholds your endpoint tests.- Run them all with
cargo test.
// tests/api.rs
use my_api::build_router; // exported from lib.rs
#[tokio::test]
async fn health_ok() {
let _app = build_router();
// send a request and assert ...
}Testing Against a Database
When handlers use sqlx, tests need a database too. Common strategies: a dedicated test database, transactions rolled back after each test, or sqlx's #[sqlx::test] macro which provisions a clean database per test automatically.
// requires sqlx test features and a DATABASE_URL
use sqlx::PgPool;
#[sqlx::test]
async fn inserts_todo(pool: PgPool) {
let todo = create(&pool, "learn rust").await.unwrap();
assert_eq!(todo.title, "learn rust");
assert_eq!(todo.done, false);
}What to Test
Aim for a balanced suite:
- Happy path: valid requests return the right status and body.
- Errors: missing resources give 404, bad input gives 400.
- Edge cases: empty lists, boundary values, duplicates.
Run cargo test in CI so regressions are caught before deployment.
Quick Check
Test your understanding of testing the API.
Recap
You learned to test a Rust REST API:
- Unit-test pure logic; use
#[tokio::test]for async code. oneshotdrives the router directly, no port needed.- Build requests with bodies and headers; collect and parse response bodies.
- Use a fresh app builder per test for isolation; put integration tests in
tests/. #[sqlx::test]provisions clean databases; cover happy paths, errors, and edge cases.
Frequently asked questions
Is the “Testing the API” lesson free?
Yes — the full text of “Testing the API” is free to read here on the web, and the Learn Rust Coding course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Learn Rust Coding course, upgrade to CoddyKit PRO.
What will I learn in “Testing the API”?
Integration tests. You practise Learn Rust Coding with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Learn Rust Coding?
No prior experience is required. Learn Rust Coding on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Testing the API” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Learn Rust Coding lesson?
Yes. Every Learn Rust Coding lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Project Setup
- Endpoints and Models
- Database Integration
- Testing the API