salvo-testing
Salvo Testing
[dev-dependencies]
salvo = { version = "0.89.3", features = ["test"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
TestClient basics
TestClient builds a RequestBuilder for each HTTP method, then send(target) runs it against a Service, Router, or Handler — no TCP bind needed. The URL scheme/host is a placeholder; only the path and query are routed.
use salvo::prelude::*;
use salvo::test::{ResponseExt, TestClient};
#[handler]
async fn hello() -> &'static str { "Hello World" }
#[tokio::test]
async fn test_hello() {
let router = Router::new().get(hello);
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/")
.send(&service).await
.take_string().await.unwrap();
assert_eq!(content, "Hello World");
}
Available methods: get, post, put, delete, patch, head, options, trace.
Builder methods
| Method | Purpose |
|---|---|
.query(k, v) / .queries(pairs) |
Append query params |
.add_header(name, value, overwrite) |
Set a header |
.basic_auth(user, Some(pass)) |
HTTP Basic auth |
.bearer_auth(token) |
Authorization: Bearer ... |
.json(&value) |
Serialize + set JSON body |
.raw_json(string) |
Pre-serialized JSON body |
.form(&value) |
Serialize + set urlencoded form |
.raw_form(string) |
Pre-serialized form body |
.text(s) / .bytes(vec) / .body(body) |
Raw bodies |
.send(target) |
Run and return Response |
target can be &Service, Router, Arc<Router>, or any Handler.
Response helpers
ResponseExt adds async consumers that take &mut Response:
take_string()— decode body as string (honors charset)take_json::<T>()— deserialize bodytake_bytes(content_type)— raw bytes
Status: res.status_code (an Option<StatusCode> field, not a method).
Path params
#[handler]
async fn show_user(req: &mut Request) -> String {
let id = req.param::<i64>("id").unwrap();
format!("User ID: {id}")
}
#[tokio::test]
async fn test_show_user() {
let router = Router::new().push(Router::with_path("users/{id}").get(show_user));
let content = TestClient::get("http://127.0.0.1:8080/users/123")
.send(&Service::new(router)).await
.take_string().await.unwrap();
assert_eq!(content, "User ID: 123");
}
Path syntax uses {name}.
JSON request/response
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct User { id: i64, name: String }
#[handler]
async fn create(req: &mut Request) -> Result<Json<User>, StatusError> {
let user: User = req.parse_json().await
.map_err(|_| StatusError::bad_request())?;
Ok(Json(user))
}
#[tokio::test]
async fn test_create() {
let router = Router::new().post(create);
let user = TestClient::post("http://127.0.0.1:8080/")
.json(&serde_json::json!({ "id": 1, "name": "Alice" }))
.send(&Service::new(router)).await
.take_json::<User>().await.unwrap();
assert_eq!(user.name, "Alice");
}
Headers and auth
#[handler]
async fn protected(req: &mut Request) -> Result<&'static str, StatusError> {
match req.header::<String>("authorization").as_deref() {
Some("Bearer valid") => Ok("ok"),
_ => Err(StatusError::unauthorized()),
}
}
#[tokio::test]
async fn test_auth() {
let router = Router::new().get(protected);
let service = Service::new(router);
let res = TestClient::get("http://127.0.0.1:8080/")
.bearer_auth("valid")
.send(&service).await;
assert_eq!(res.status_code, Some(StatusCode::OK));
let res = TestClient::get("http://127.0.0.1:8080/")
.send(&service).await;
assert_eq!(res.status_code, Some(StatusCode::UNAUTHORIZED));
}
Middleware and Depot
#[handler]
async fn setup(depot: &mut Depot) { depot.insert("flag", true); }
#[handler]
async fn read(depot: &mut Depot) -> String {
let flag = *depot.get::<bool>("flag").unwrap();
format!("flag={flag}")
}
#[tokio::test]
async fn test_depot() {
let router = Router::new().hoop(setup).get(read);
let content = TestClient::get("http://127.0.0.1:8080/")
.send(&Service::new(router)).await
.take_string().await.unwrap();
assert_eq!(content, "flag=true");
}
Note: depot.get::<T>(key) returns Result<&T, _>, not Option. Unwrap and deref.
Form data
let res = TestClient::post("http://127.0.0.1:8080/")
.form(&[("name", "Alice"), ("email", "alice@example.com")])
.send(&service).await;
Full CRUD flow
#[tokio::test]
async fn test_crud() {
let service = Service::new(create_router());
let res = TestClient::post("http://127.0.0.1:8080/users")
.json(&serde_json::json!({"name": "Alice"}))
.send(&service).await;
assert_eq!(res.status_code, Some(StatusCode::CREATED));
let user = TestClient::get("http://127.0.0.1:8080/users/1")
.send(&service).await
.take_json::<User>().await.unwrap();
assert_eq!(user.name, "Alice");
let res = TestClient::delete("http://127.0.0.1:8080/users/1")
.send(&service).await;
assert_eq!(res.status_code, Some(StatusCode::NO_CONTENT));
}
Gotchas
send()takes the target by value forRouter/Handler, but by&Servicefor a service. Build aServiceonce if you reuse it across requests.res.status_codeis a field (Option<StatusCode>), not a method call.- The URL host/port in
TestClient::get(url)is ignored by routing — only the path and query matter. take_string/take_jsonconsume the body; a second call returns empty.
Related Skills
- salvo-error-handling: Assert
StatusErrorresponses map to the expected codes. - salvo-auth: Use
.bearer_auth()/.basic_auth()for protected endpoints. - salvo-database: Integration-test real repositories by passing a test pool via
affix_state.
More from salvo-rs/salvo-skills
salvo-csrf
Implement CSRF (Cross-Site Request Forgery) protection using cookie or session storage. Use for protecting forms and state-changing endpoints.
16salvo-auth
Implement authentication and authorization using JWT, Basic Auth, or custom schemes. Use for securing API endpoints and user management.
15salvo-cors
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
15salvo-middleware
Implement middleware for authentication, logging, CORS, and request processing. Use for cross-cutting concerns and request/response modification.
15salvo-realtime
Implement real-time features using WebSocket and Server-Sent Events (SSE). Use for chat applications, live updates, notifications, and bidirectional communication.
15salvo-caching
Implement caching strategies for improved performance. Use for reducing database load and speeding up responses.
15