# Testing & Configuration

You've grown the books API from a single attribute route into full CRUD with error catchers. Two questions turn a weekend project into something you'd let other people depend on: *how do I know a change didn't quietly break a route?* and *how do I run this somewhere other than my laptop, with the right port and settings?* This phase answers both.

📝 **Testing and configuration are the two things that make your app portable** - able to run somewhere other than the place you wrote it. A test runs your *real* app in a throwaway, in-process environment so you can poke it and check the answers. Configuration is how that same app behaves differently depending on where it runs - port 8000 on your machine, port 80 in production - without editing code. Both lean on the same trick: building your `Rocket` instance from a function you can call from anywhere.

## The test client: call your app without a server

The fear most people bring to testing a web app is that they'll have to start the server, fire real HTTP requests at `localhost`, then tear it all down - slow, flaky, full of "connection refused" when something didn't boot in time. Rocket sidesteps that.

📝 **Rocket's local `Client` runs your real application in-process and dispatches requests straight to it - no network, no port, no running server.** Hand it the same `Rocket` instance your `main` would launch, call `client.get("/books")`, and Rocket routes that request through your actual handlers, guards, and catchers, then hands you back a response. A function call wearing an HTTP costume - fast (milliseconds) and reliable (nothing to boot, nothing to crash).

There are two flavors. The **blocking** client (`rocket::local::blocking::Client`) is the friendliest for tests - no `async` ceremony. Here's the shape, using the books API:

```rust
use rocket::local::blocking::Client;
use rocket::http::Status;

#[test]
fn list_books_ok() {
    let client = Client::tracked(rocket()).expect("valid rocket");
    let res = client.get("/books").dispatch();
    assert_eq!(res.status(), Status::Ok);
    // res.into_json::<Vec<Book>>() to read the body back as typed data
}
```

*What just happened:* `Client::tracked(rocket())` takes your built application - `rocket()` returns the same builder your `#[launch]` uses (factored out in a moment) - and wraps it in a test client. `client.get("/books")` builds a request; `.dispatch()` runs it through the app and returns the response. We assert on `res.status()`: did it succeed, redirect, 404? The `tracked` part means the client remembers cookies across requests, which matters for login or sessions - `Client::untracked(...)` exists for when you don't care.

To check the *body*, not the status, read it back as typed data:

```rust
#[test]
fn list_books_returns_json() {
    let client = Client::tracked(rocket()).expect("valid rocket");
    let res = client.get("/books").dispatch();

    let books = res.into_json::<Vec<Book>>().expect("valid book list");
    assert!(books.iter().any(|b| b.title == "Dune"));
}
```

*What just happened:* `res.into_json::<Vec<Book>>()` deserializes the response body into your real `Book` type - the same `serde` derive your handlers use, now working in reverse. You get back actual Rust values to assert against: "the list contains a book titled Dune." If the body isn't valid JSON for that type, `into_json` returns `None` and `.expect(...)` fails loudly.

Writing data is the same idea with a request body attached:

```rust
#[test]
fn create_book_returns_201() {
    let client = Client::tracked(rocket()).expect("valid rocket");
    let new_book = NewBook { title: "Hyperion".into(), author: "Simmons".into() };

    let res = client.post("/books").json(&new_book).dispatch();

    assert_eq!(res.status(), Status::Created);
    let created = res.into_json::<Book>().expect("returns the created book");
    assert_eq!(created.title, "Hyperion");
}
```

*What just happened:* `client.post("/books").json(&new_book)` serializes your `NewBook` to JSON and sets `Content-Type: application/json` - the mirror image of the `Json<NewBook>` data guard. We assert the status is `201 Created` and that the response echoes back the created book. This single test exercises routing, the JSON data guard, handler logic, and the responder - the whole vertical slice, no server in sight.

> 💡 There's also an async client, `rocket::local::asynchronous::Client`, same API but `.dispatch().await`. Reach for it when the test itself needs to be `async`. For most route tests the blocking client is less ceremony - start there.

If you've never written a Rust test before - `#[test]`, `assert_eq!`, `cargo test`, Arrange-Act-Assert - see [Your First Unit Test](/guides/your-first-unit-test), and [Testing in CI](/guides/testing-in-ci) for wiring `cargo test` into a pipeline.

## Factor the builder so tests and `main` share one app

Every test above calls `rocket()`. That isn't a coincidence - it's the load-bearing pattern of the whole phase. ⚠️ **If your `#[launch]` function builds the app inline, your tests can't get at it - they'd have to duplicate the build, and a duplicate drifts.** The instant your test app is configured even slightly differently from the real one, your tests are lying to you.

The fix is to build the app in *one* place that both `main` and the tests call:

```rust
use rocket::{Build, Rocket};

fn rocket() -> Rocket<Build> {
    rocket::build()
        .mount("/", routes![list_books, get_book, create_book, delete_book])
        .register("/", catchers![not_found, unprocessable])
        .manage(BookStore::new())
}

#[launch]
fn launch() -> Rocket<Build> {
    rocket()
}
```

*What just happened:* the real assembly - mounting routes, registering catchers, managing state - now lives in `rocket()`, which returns a `Rocket<Build>` (the "configured but not yet launched" stage). `#[launch]` does nothing but call it. Production launches exactly what your tests dispatch against, down to the last catcher. 📝 Same principle as Flask's app factory: one function builds the app, and everything - tests, `main`, later a benchmark harness - asks *it* for a fresh instance. One source of truth, no drift.

## Configuration: `Rocket.toml`, profiles, and env vars

Your app shouldn't hard-code where it runs. Port 8000 is fine on your laptop; production might want port 80, a different bind address, quieter logs. Rocket handles this through **Figment**, its layered configuration system: mostly a `Rocket.toml` file plus environment variables.

📝 **Rocket reads `Rocket.toml` from your project root, split into *profiles* - named sections like `[default]`, `[debug]`, and `[release]` - and the active profile is chosen by how you built the app.** A debug build (`cargo run`) uses `debug`; a release build (`cargo run --release`) uses `release`. `[default]` applies to *every* profile as a baseline, and the active profile overrides it.

```toml
[default]
address = "0.0.0.0"
port = 8000

[release]
port = 80
```

*What just happened:* `[default]` sets the baseline - bind to `0.0.0.0` (all interfaces) on port 8000 - so a normal `cargo run` serves on 8000. Build with `--release` and Rocket layers `[release]` on top, overriding the port to 80 while keeping the inherited `address`. One file describes both environments; the build flag picks which one is live. No `if cfg!(debug)` branching in your code.

The second layer is environment variables, sitting *on top* of the file:

```bash
ROCKET_PORT=9000 cargo run
```

*What just happened:* Rocket reads any `ROCKET_`-prefixed variable as a config override, so this runs on port 9000 regardless of `Rocket.toml` - the env var wins. Common ones: `ROCKET_PORT`, `ROCKET_ADDRESS`, `ROCKET_LOG_LEVEL` (try `=debug` when something's misbehaving, `=off` to silence it). 💡 This precedence - env vars over file over defaults - is exactly what deployment wants: bake sane values into `Rocket.toml`, then let the host override the few that differ (the port a platform assigns you) without rebuilding.

You can also add your *own* keys and read them as a typed struct:

```rust
use serde::Deserialize;

#[derive(Deserialize)]
struct AppConfig {
    catalog_name: String,
    max_books: usize,
}

// inside a fairing or AdHoc::config, given the built `rocket`:
let app_config: AppConfig = rocket.figment().extract().expect("valid app config");
```

*What just happened:* you put `catalog_name` and `max_books` under a profile in `Rocket.toml`, and `rocket.figment().extract()` deserializes the *whole* active configuration into your `AppConfig` struct - your custom keys plus Rocket's own. Same `serde` machinery as your JSON bodies, pointed at config instead. In practice you do this in a fairing (Phase 5) or via `AdHoc::config::<AppConfig>()`, so the parsed config becomes managed state your handlers pull in with `State<AppConfig>`. Type-checked at startup, no scattered `env::var` calls.

## Production: build for release and let the host override

Shipping the books API comes down to a few moves, and you've already met most of the pieces.

**Build for release**: `cargo build --release` compiles with optimizations into `target/release/`, and running that binary activates the `release` profile from `Rocket.toml` - the port-80 setting and any other release tuning come along automatically. **Let the host override what's environment-specific** through `ROCKET_*` vars: a platform that hands you a port at runtime sets `ROCKET_PORT`, and your app obeys without a rebuild.

To run the same everywhere, wrap it in a container. Rust compiles to a single binary, so use a **multi-stage build**: one stage compiles, a tiny final stage carries only the binary.

```dockerfile
FROM rust:1.82 AS build
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
WORKDIR /app
COPY --from=build /app/target/release/books-api .
COPY Rocket.toml .
ENV ROCKET_ADDRESS=0.0.0.0
CMD ["./books-api"]
```

*What just happened:* the `build` stage has the full Rust toolchain and compiles your release binary; the final image starts from a slim Debian base and copies *only* the compiled binary and `Rocket.toml` across - the multi-gigabyte compiler toolchain never ships. `ENV ROCKET_ADDRESS=0.0.0.0` makes the server listen on all interfaces inside the container (the default `127.0.0.1` only accepts connections from *within* it). The result is a small, self-contained image that runs identically on your laptop and a cloud host.

⚠️ **Rocket is an application server, not a front door.** In production put a **reverse proxy** (nginx or your platform's load balancer) in front of it to terminate TLS and shield the app from raw internet traffic - Rocket serves plain HTTP behind it. For the full walk from "binary that runs" to "live on a domain with HTTPS," see [Ship Your Side Project](/guides/ship-your-side-project).

💡 Clean testing and clean configuration are the *same* capability wearing two hats: your app is a value you build from one function and run in different contexts - dispatched in-process by a test `Client`, or launched with a release profile behind a proxy. The `rocket()` function you factored out is what makes the app portable enough to ship.

## Recap

1. 📝 **The local `Client` dispatches requests to your real app in-process** - no server, no port. Use `rocket::local::blocking::Client`, `Client::tracked(rocket())`, then `client.get(...).dispatch()`. Assert on `res.status()`; read the body with `res.into_json::<T>()`.
2. **POST tests send a body with `.json(&value)`**, which serializes it and sets the JSON content type - the mirror of your `Json<T>` data guard. There's also an async client (`rocket::local::asynchronous::Client`) for when the test itself is `async`.
3. ⚠️ **Factor the build into a `fn rocket() -> Rocket<Build>`** that both `#[launch]` and your tests call, so production and tests run the *same* app. Inline-built apps force a duplicate that drifts.
4. 📝 **Configure with `Rocket.toml` profiles** (`[default]` baseline, `[debug]`, `[release]`); the build flag (`--release`) picks the active profile. **`ROCKET_*` env vars override the file** (`ROCKET_PORT`, `ROCKET_ADDRESS`, `ROCKET_LOG_LEVEL`) - precedence is env > file > defaults.
5. **Custom config keys** parse into your own struct via `rocket.figment().extract()`, usually in a fairing or `AdHoc::config`, becoming managed state.
6. **Production:** `cargo build --release` activates the `release` profile; let the host set `ROCKET_*`; ship a multi-stage container carrying only the binary; put a reverse proxy in front for TLS.

## Quick check

Three questions on the ideas that matter most before you ship:

```quiz
[
  {
    "q": "What does Rocket's local Client (rocket::local::blocking::Client) let you do?",
    "choices": [
      "Dispatch requests to your real application in-process - through your actual routes, guards, and catchers - with no server, port, or network",
      "Start a real server on a random port and send it HTTP requests over the loopback network",
      "Generate test cases automatically for every mounted route",
      "Connect your tests to the production database so they exercise real rows"
    ],
    "answer": 0,
    "explain": "The local Client runs your built Rocket instance in-process and dispatches requests straight to it - no server, no port, no network. You assert on res.status() and read bodies with res.into_json::<T>(). That's what makes the tests fast and reliable."
  },
  {
    "q": "Why factor app construction into a function like fn rocket() -> Rocket<Build> that both #[launch] and your tests call?",
    "choices": [
      "So tests and production build the exact same app from one source of truth, with no duplicated setup that can drift apart",
      "Because #[launch] is not allowed to call .mount() or .manage() directly",
      "Because the local Client can only accept a function named rocket()",
      "To make the release build smaller by removing the launch macro"
    ],
    "answer": 0,
    "explain": "If #[launch] builds the app inline, tests have to duplicate the build - and any drift between the two means your tests no longer reflect production. One rocket() function that both call keeps them identical."
  },
  {
    "q": "In Rocket's configuration, what overrides a value set in Rocket.toml?",
    "choices": [
      "A matching ROCKET_* environment variable - env vars sit on top of the file, so ROCKET_PORT=9000 wins over the file's port",
      "Nothing; values in Rocket.toml are final and cannot be overridden at runtime",
      "The [default] profile always wins over every other profile and over env vars",
      "Command-line flags passed to cargo run, which Rocket parses directly"
    ],
    "answer": 0,
    "explain": "Rocket layers config via Figment with precedence env > file > defaults. A ROCKET_-prefixed env var (e.g. ROCKET_PORT=9000) overrides whatever Rocket.toml says, which is exactly what you want for letting a host set the port at deploy time without a rebuild."
  }
]
```
