大家是否認為使用 curl 的測試效率太低,也不好檢查結果,現在我們將要學習高效的測試方式。
先在 main.rs 或一個 lib.rs 提供一個建立 app 的 function,方便測試時呼叫:
src/app.rs (新增)
use axum::{Router, routing::{get, post, put, delete}, Extension};
use sqlx::PgPool;
use redis::aio::MultiplexedConnection;
pub fn create_app(pool: PgPool, redis: MultiplexedConnection) -> Router {
Router::new()
.route("/users", post(crate::handlers::create_user).get(crate::handlers::list_users))
.route("/users/{id}", get(crate::handlers::get_user).put(crate::handlers::update_user).delete(crate::handlers::delete_user))
.route("/users/login", post(crate::handlers::login))
.layer(Extension(pool))
.layer(Extension(redis))
}
新增 src/lib.rs,以便測試可以使用 create_app
pub mod handlers;
pub mod models;
pub mod cache;
pub mod password;
pub mod app;
在 Cargo.toml 添加使用的依賴套件
[dev-dependencies]
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
uuid = { version = "1", features = ["v4"] }
整合測試(放在 tests/integration_tests.rs)
tests/integration_tests.rs
use reqwest::StatusCode;
use sqlx::{PgPool, Executor};
use std::net::SocketAddr;
use tokio::net::TcpListener;
use uuid::Uuid;
use sqlx::migrate::MigrateDatabase;
use serde_json::Value;
use redis::Client as RedisClient;
async fn spawn_app() -> (String, PgPool, redis::aio::MultiplexedConnection) {
// 建立測試資料庫(用 TEST_DATABASE_URL 隔離真實資料庫)
let base_url = std::env::var("TEST_DATABASE_URL_BASE").expect("TEST_DATABASE_URL_BASE");
let db_name = format!("test_db_{}", Uuid::new_v4().to_string().replace("-", ""));
let db_url = format!("{}/{}", base_url, db_name);
if !sqlx::postgres::Postgres::database_exists(&db_url).await.unwrap_or(false) {
sqlx::Postgres::create_database(&db_url).await.unwrap();
}
let pool = PgPool::connect(&db_url).await.unwrap();
// 執行 migrations
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
// Redis - 指定 TEST_REDIS_URL,隔離開發環境
let redis_url = std::env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379/1".to_string());
let redis_client = RedisClient::open(redis_url.as_str()).unwrap();
let redis_conn = redis_client.get_multiplexed_tokio_connection().await.unwrap();
// 使用不同 port 避免占用真實專案的 port
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.unwrap();
let addr = listener.local_addr().unwrap();
let app = sqlx_connect_demo::app::create_app(pool.clone(), redis_conn.clone());
let server_handle = tokio::spawn(async move {
axum::serve(listener, app)
.await
.unwrap();
});
(format!("http://{}", addr), pool, redis_conn)
}
#[tokio::test]
async fn integration_create_get_update_delete_user_flow() {
let (base_url, pool, _redis) = spawn_app().await;
let client = reqwest::Client::new();
// POST /users (create)
let res = client.post(format!("{}/users", base_url))
.json(&serde_json::json!({
"username": "user1",
"email": "user1@a.com",
"password": "password"
}))
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
let body: Value = res.json().await.unwrap();
let id = body["id"].as_i64().unwrap();
// GET /users/{id} - 成功
let res = client.get(format!("{}/users/{}", base_url, id)).send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// PUT /users/{id} - 成功
let res = client.put(format!("{}/users/{}", base_url, id))
.json(&serde_json::json!({"username": "new_user1"}))
.send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// POST /users/login -> 成功
let res = client.post(format!("{}/users/login", base_url))
.json(&serde_json::json!({
"username_or_email": "new_user1",
"password": "password"
}))
.send().await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
// DELETE /users/{id} -> 成功
let res = client.delete(format!("{}/users/{}", base_url, id)).send().await.unwrap();
assert_eq!(res.status(), StatusCode::NO_CONTENT);
// GET 已被刪除的資料 -> 404
let res = client.get(format!("{}/users/{}", base_url, id)).send().await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn integration_login_failures() {
let (base_url, _pool, _redis) = spawn_app().await;
let client = reqwest::Client::new();
// 登入不存在的用戶 -> 401
let res = client.post(format!("{}/users/login", base_url))
.json(&serde_json::json!({
"username_or_email": "not_exists",
"password": "password"
}))
.send().await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
// 建立用戶 -> 成功
let res = client.post(format!("{}/users", base_url))
.json(&serde_json::json!({
"username": "user1",
"email": "user1@a.com",
"password": "truepass"
}))
.send()
.await.unwrap();
assert_eq!(res.status(), StatusCode::CREATED);
// 登入,但密碼錯誤 -> 401
let res = client.post(format!("{}/users/login", base_url))
.json(&serde_json::json!({
"username_or_email": "user1",
"password": "falsepass"
}))
.send()
.await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
這些測試涵蓋:
注意:
set TEST_DATABASE_URL_BASE=DATABASE_URL=postgres://user:password@127.0.0.1:5432
cargo test --test integration_tests
輸出結果
running 2 tests
test integration_login_failures ... ok
test integration_create_get_update_delete_user_flow ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.39s
可以看到兩個測試都通過了