Skip to main content
Lesson 11 of 12Beginner⏱ 10 mins

Testing the API

Automated tests.

83% of path

Learning Objectives

  • Write an integration test for a route

##Testing without starting a server

Automated tests catch bugs before your users do. Actix Web includes the actix_web::test module so you can send fake requests straight into your app in memory, with no port, no network and no flakiness.

#A first test

Write the handler, then test it in the same file:

code_blocksrust
use actix_web::{get, App, HttpResponse, Responder};

#[get("/hello")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("hello")
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{http::StatusCode, test};

    #[actix_web::test]
    async fn hello_returns_ok() {
        let app = test::init_service(App::new().service(hello)).await;

        let req = test::TestRequest::get().uri("/hello").to_request();
        let resp = test::call_service(&app, req).await;

        assert_eq!(resp.status(), StatusCode::OK);

        let body = test::read_body(resp).await;
        assert_eq!(body, "hello");
    }
}

Run all tests with:

terminalbash
cargo test

#The key pieces

  • #[actix_web::test] is like #[test] but starts an async runtime for you.
  • test::init_service builds your App as a callable service.
  • TestRequest builds a request: choose the method, URI, headers and body.
  • call_service sends it and returns the response, so you can assert on status, headers and body.

#Testing JSON endpoints

code_blocksrust
use actix_web::test;
use serde_json::json;

let req = test::TestRequest::post()
    .uri("/users")
    .set_json(json!({ "name": "Ada" }))
    .to_request();

let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());

let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(body["name"], "Ada");

#Testing with state

Register the same app_data you use in production, but with test values (for example an in-memory fake or a test database):

code_blocksrust
let app = test::init_service(
    App::new()
        .app_data(web::Data::new(AppState { app_name: "test".into() }))
        .service(index),
)
.await;

#Good habits

  • Test both the happy path and the error cases (404, 400, 401).
  • Keep an fn configure(cfg: &mut web::ServiceConfig) that registers your routes, and call it from both main and your tests so they always match.
  • Keep tests small and independent; each one builds its own app.

Interactive Code Inspector

Select any highlighted line of code below to deeply inspect its mechanics, understand why it exists, and what happens if you change it.

src/main.rs
1#[cfg(test)]
2mod tests {
3 use super::*;
4 use actix_web::{test, App};
5
6 #[actix_web::test]
7 async fn test_index_get() {
8 let app = test::init_service(App::new().service(index)).await;
9 let req = test::TestRequest::get().uri("/").to_request();
10 let resp = test::call_service(&app, req).await;
11 assert!(resp.status().is_success());
12 }
13}
Line 8
test::init_service(...)

What it does

Initializes a mock application service for testing.

Why use it?

It avoids actually spinning up a TCP server on a port.

How it works underneath

Runs the app entirely in memory.

If you remove it

You cannot test routes without manually creating an HTTP client and spinning up the server.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Break a test on purpose.

Starting Code
assert_eq!(body, "hello");
Suggested Modification

Change the expected text to "goodbye" and run cargo test.

Expected Result

The test fails and prints both the left and right values.

Explanation: Seeing a test fail teaches you how to read the failure output, which you will rely on when real bugs appear.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Using #[test] for an async test

Plain #[test] has no async runtime, so await cannot run.

Incorrect Pattern
#[test] async fn my_test() { /* ... */ }
Correct Pattern
#[actix_web::test] async fn my_test() { /* ... */ }

Explanation: actix_web::test sets up the runtime that async code needs.

!

Forgetting to register app_data in the test app

The handler asks for web::Data<T> but the test App never provided it, so every request fails with 500.

Incorrect Pattern
test::init_service(App::new().service(index))
Correct Pattern
test::init_service(App::new().app_data(web::Data::new(state)).service(index))

Explanation: The test App must contain everything the real App provides, including state and middleware the handler relies on.

Practical Exercise

Test a 404

Task

Write a test that requests a route that does not exist and asserts the status is 404.

Requirements

  • Build an App with one route
  • Request a different URI
  • Assert StatusCode::NOT_FOUND

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Teams run these in-memory tests in CI on every pull request. They run in milliseconds, so thousands of checks cost nothing, and they catch broken routes, wrong status codes and missing auth before deployment.

Key Takeaways

  • Use #[actix_web::test] and cargo test for async tests.
  • init_service builds the app, TestRequest builds requests, call_service sends them.
  • test::read_body and read_body_json help you assert on the response body.
  • Share a configure function between main and tests so routes never drift apart.

Finished reading?

Mark this lesson as done to track your progress and earn XP.