salvo-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSalvo Testing
Salvo 测试
This skill helps write tests for Salvo applications using the built-in testing utilities.
本技能介绍如何使用Salvo内置的测试工具为Salvo应用编写测试。
Setup
环境配置
Add to :
Cargo.tomltoml
[dev-dependencies]
salvo = { version = "0.89.0", features = ["test"] }
tokio-test = "0.4"添加以下内容到:
Cargo.tomltoml
[dev-dependencies]
salvo = { version = "0.89.0", features = ["test"] }
tokio-test = "0.4"Basic Handler Testing
基础处理器测试
rust
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");
}rust
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");
}Testing with Path Parameters
路径参数测试
rust
#[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 service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/users/123")
.send(&service)
.await
.take_string()
.await
.unwrap();
assert_eq!(content, "User ID: 123");
}rust
#[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 service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/users/123")
.send(&service)
.await
.take_string()
.await
.unwrap();
assert_eq!(content, "User ID: 123");
}Testing JSON Responses
JSON响应测试
rust
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct User {
id: i64,
name: String,
}
#[handler]
async fn get_user() -> Json<User> {
Json(User {
id: 1,
name: "Alice".to_string(),
})
}
#[tokio::test]
async fn test_get_user() {
let router = Router::new().get(get_user);
let service = Service::new(router);
let user = TestClient::get("http://127.0.0.1:8080/")
.send(&service)
.await
.take_json::<User>()
.await
.unwrap();
assert_eq!(user.id, 1);
assert_eq!(user.name, "Alice");
}rust
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct User {
id: i64,
name: String,
}
#[handler]
async fn get_user() -> Json<User> {
Json(User {
id: 1,
name: "Alice".to_string(),
})
}
#[tokio::test]
async fn test_get_user() {
let router = Router::new().get(get_user);
let service = Service::new(router);
let user = TestClient::get("http://127.0.0.1:8080/")
.send(&service)
.await
.take_json::<User>()
.await
.unwrap();
assert_eq!(user.id, 1);
assert_eq!(user.name, "Alice");
}Testing POST Requests
POST请求测试
rust
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[handler]
async fn create_user(body: JsonBody<CreateUser>) -> StatusCode {
let user = body.into_inner();
// Save user...
StatusCode::CREATED
}
#[tokio::test]
async fn test_create_user() {
let router = Router::new().post(create_user);
let service = Service::new(router);
let res = TestClient::post("http://127.0.0.1:8080/")
.json(&serde_json::json!({
"name": "Bob",
"email": "bob@example.com"
}))
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::CREATED));
}rust
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[handler]
async fn create_user(body: JsonBody<CreateUser>) -> StatusCode {
let user = body.into_inner();
// 保存用户...
StatusCode::CREATED
}
#[tokio::test]
async fn test_create_user() {
let router = Router::new().post(create_user);
let service = Service::new(router);
let res = TestClient::post("http://127.0.0.1:8080/")
.json(&serde_json::json!({
"name": "Bob",
"email": "bob@example.com"
}))
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::CREATED));
}Testing with Headers
请求头测试
rust
#[handler]
async fn protected(req: &mut Request) -> Result<&'static str, StatusError> {
let token = req.header::<String>("Authorization")
.ok_or_else(|| StatusError::unauthorized())?;
if token == "Bearer valid_token" {
Ok("Protected content")
} else {
Err(StatusError::unauthorized())
}
}
#[tokio::test]
async fn test_protected_with_valid_token() {
let router = Router::new().get(protected);
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/")
.add_header("Authorization", "Bearer valid_token", true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert_eq!(content, "Protected content");
}
#[tokio::test]
async fn test_protected_without_token() {
let router = Router::new().get(protected);
let service = Service::new(router);
let res = TestClient::get("http://127.0.0.1:8080/")
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::UNAUTHORIZED));
}rust
#[handler]
async fn protected(req: &mut Request) -> Result<&'static str, StatusError> {
let token = req.header::<String>("Authorization")
.ok_or_else(|| StatusError::unauthorized())?;
if token == "Bearer valid_token" {
Ok("Protected content")
} else {
Err(StatusError::unauthorized())
}
}
#[tokio::test]
async fn test_protected_with_valid_token() {
let router = Router::new().get(protected);
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/")
.add_header("Authorization", "Bearer valid_token", true)
.send(&service)
.await
.take_string()
.await
.unwrap();
assert_eq!(content, "Protected content");
}
#[tokio::test]
async fn test_protected_without_token() {
let router = Router::new().get(protected);
let service = Service::new(router);
let res = TestClient::get("http://127.0.0.1:8080/")
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::UNAUTHORIZED));
}Testing Middleware
中间件测试
rust
#[handler]
async fn logger(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
depot.insert("logged", true);
ctrl.call_next(req, depot, res).await;
}
#[handler]
async fn handler(depot: &mut Depot) -> String {
let logged = depot.get::<bool>("logged").copied().unwrap_or(false);
format!("Logged: {}", logged)
}
#[tokio::test]
async fn test_middleware() {
let router = Router::new()
.hoop(logger)
.get(handler);
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, "Logged: true");
}rust
#[handler]
async fn logger(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
depot.insert("logged", true);
ctrl.call_next(req, depot, res).await;
}
#[handler]
async fn handler(depot: &mut Depot) -> String {
let logged = depot.get::<bool>("logged").copied().unwrap_or(false);
format!("Logged: {}", logged)
}
#[tokio::test]
async fn test_middleware() {
let router = Router::new()
.hoop(logger)
.get(handler);
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, "Logged: true");
}Testing with Query Parameters
查询参数测试
rust
#[handler]
async fn search(req: &mut Request) -> String {
let query = req.query::<String>("q").unwrap_or_default();
format!("Search: {}", query)
}
#[tokio::test]
async fn test_search() {
let router = Router::new().get(search);
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/?q=rust")
.send(&service)
.await
.take_string()
.await
.unwrap();
assert_eq!(content, "Search: rust");
}rust
#[handler]
async fn search(req: &mut Request) -> String {
let query = req.query::<String>("q").unwrap_or_default();
format!("Search: {}", query)
}
#[tokio::test]
async fn test_search() {
let router = Router::new().get(search);
let service = Service::new(router);
let content = TestClient::get("http://127.0.0.1:8080/?q=rust")
.send(&service)
.await
.take_string()
.await
.unwrap();
assert_eq!(content, "Search: rust");
}Testing Error Handling
错误处理测试
rust
#[handler]
async fn may_fail(req: &mut Request) -> Result<String, StatusError> {
let id = req.param::<i64>("id").unwrap();
if id == 0 {
return Err(StatusError::bad_request());
}
Ok(format!("ID: {}", id))
}
#[tokio::test]
async fn test_error_handling() {
let router = Router::new()
.push(Router::with_path("{id}").get(may_fail));
let service = Service::new(router);
let res = TestClient::get("http://127.0.0.1:8080/0")
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::BAD_REQUEST));
}rust
#[handler]
async fn may_fail(req: &mut Request) -> Result<String, StatusError> {
let id = req.param::<i64>("id").unwrap();
if id == 0 {
return Err(StatusError::bad_request());
}
Ok(format!("ID: {}", id))
}
#[tokio::test]
async fn test_error_handling() {
let router = Router::new()
.push(Router::with_path("{id}").get(may_fail));
let service = Service::new(router);
let res = TestClient::get("http://127.0.0.1:8080/0")
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::BAD_REQUEST));
}Testing with Depot
Depot测试
rust
#[handler]
fn setup_depot(depot: &mut Depot) {
depot.insert("config", "test_value");
}
#[handler]
async fn use_depot(depot: &mut Depot) -> String {
let config = depot.get::<&str>("config").copied().unwrap();
format!("Config: {}", config)
}
#[tokio::test]
async fn test_depot() {
let router = Router::new()
.hoop(setup_depot)
.get(use_depot);
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, "Config: test_value");
}rust
#[handler]
fn setup_depot(depot: &mut Depot) {
depot.insert("config", "test_value");
}
#[handler]
async fn use_depot(depot: &mut Depot) -> String {
let config = depot.get::<&str>("config").copied().unwrap();
format!("Config: {}", config)
}
#[tokio::test]
async fn test_depot() {
let router = Router::new()
.hoop(setup_depot)
.get(use_depot);
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, "Config: test_value");
}Testing Form Data
表单数据测试
rust
#[tokio::test]
async fn test_form_submission() {
let router = Router::new().post(handle_form);
let service = Service::new(router);
let res = TestClient::post("http://127.0.0.1:8080/")
.form(&[("name", "Alice"), ("email", "alice@example.com")])
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::OK));
}rust
#[tokio::test]
async fn test_form_submission() {
let router = Router::new().post(handle_form);
let service = Service::new(router);
let res = TestClient::post("http://127.0.0.1:8080/")
.form(&[("name", "Alice"), ("email", "alice@example.com")])
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::OK));
}Integration Testing
集成测试
rust
#[tokio::test]
async fn test_full_crud() {
let router = create_router();
let service = Service::new(router);
// Create
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));
// Read
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");
// Update
let res = TestClient::patch("http://127.0.0.1:8080/users/1")
.json(&serde_json::json!({"name": "Alice Updated"}))
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::OK));
// Delete
let res = TestClient::delete("http://127.0.0.1:8080/users/1")
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::NO_CONTENT));
}rust
#[tokio::test]
async fn test_full_crud() {
let router = create_router();
let service = Service::new(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::patch("http://127.0.0.1:8080/users/1")
.json(&serde_json::json!({"name": "Alice Updated"}))
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::OK));
// 删除
let res = TestClient::delete("http://127.0.0.1:8080/users/1")
.send(&service)
.await;
assert_eq!(res.status_code(), Some(StatusCode::NO_CONTENT));
}Best Practices
最佳实践
- Test each handler in isolation
- Test middleware separately from handlers
- Test error cases and edge cases
- Use descriptive test names
- Test with different HTTP methods
- Verify status codes and response bodies
- Test authentication and authorization
- Mock external dependencies
- Use test fixtures for complex data
- Run tests in parallel when possible
- 单独测试每个处理器
- 将中间件与处理器分开测试
- 测试错误场景与边缘情况
- 使用描述性的测试名称
- 测试不同的HTTP方法
- 验证状态码与响应体
- 测试认证与授权逻辑
- 模拟外部依赖
- 为复杂数据使用测试夹具
- 尽可能并行运行测试