在建構 RESTful API 時,獲取單一資源的詳細資訊是一個最基本也最常見的需求。本文將以「測試驅動開發」(TDD) 的思想為指導,帶您從零開始,為一個課程平台設計、實現並徹底測試 GET /courses/{tutor_id}/{course_id} 這個 API 端點。我們將使用記憶體中的應用程式狀態來模擬資料庫,讓您能專注於 Actix-web 的核心開發與測試技巧。
步驟一:定義 API 需求與資料模型
我們的目標是建立一個 API,它能根據傳入的 tutor_id 和 course_id,從共享的應用程式狀態中查找並回傳對應的課程資料。
首先,我們需要 Course 和 AppState 的定義:
// 在 src/models.rs
use serde::Serialize;
#[derive(Serialize, Debug, Clone)]
pub struct Course {
pub tutor_id: i32,
pub course_id: Option<i32>,
// ... 其他欄位
}
// 在 src/state.rs
use std::sync::Mutex;
pub struct AppState {
pub courses: Mutex<Vec<Course>>,
// ... 其他狀態
}
步驟二:編寫單元測試 (測試先行)
在 TDD 中,我們在編寫 handler 之前先定義它的預期行為。我們需要考慮兩種主要情境:成功找到課程,以及找不到課程。
在 src/handlers.rs 的測試模組中,我們編寫以下測試:
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{http::StatusCode, web, App};
use chrono::Utc;
use std::sync::Mutex;
// 測試成功找到課程的案例
#[actix_rt::test]
async fn get_course_detail_success_test() {
// 1. 準備: 建立一個包含目標課程的 AppState
let course = Course { tutor_id: 1, course_id: Some(1), course_name: "Test Course".into(), posted_time: Some(Utc::now().naive_utc()) };
let app_state = web::Data::new(AppState {
courses: Mutex::new(vec![course]),
// ... 其他狀態
});
// 準備 handler 需要的路徑參數
let params: web::Path<(i32, i32)> = web::Path::from((1, 1));
// 2. 執行: 直接呼叫 handler 函式
let resp = get_course_detail(app_state, params).await;
// 3. 斷言: 驗證回應狀態碼是否為 200 OK
assert_eq!(resp.status(), StatusCode::OK);
}
// 測試找不到課程的案例
#[actix_rt::test]
async fn get_course_detail_failure_test() {
// 1. 準備: 建立一個空的 AppState
let app_state = web::Data::new(AppState {
courses: Mutex::new(vec![]),
// ... 其他狀態
});
// 準備一個不存在的課程 ID
let params: web::Path<(i32, i32)> = web::Path::from((1, 999));
// 2. 執行
let resp = get_course_detail(app_state, params).await;
// 3. 斷言: 驗證回應狀態碼是否為 404 NOT FOUND
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
}
此時執行 cargo test,測試會因找不到 get_course_detail 函式而失敗。這是 TDD 的「紅燈」階段,驅使我們去創建這個函式。
步驟三:實現 API Handler
現在,我們來編寫 get_course_detail handler,目標是讓剛剛的測試通過。
// 在 src/handlers.rs
pub async fn get_course_detail(
app_state: web::Data<AppState>,
params: web::Path<(i32, i32)>,
) -> HttpResponse {
let (tutor_id, course_id) = params.into_inner();
let selected_course = app_state
.courses
.lock()
.unwrap()
.clone()
.into_iter()
.find(|x| x.tutor_id == tutor_id && x.course_id == Some(course_id));
// 根據查詢結果回傳不同的 HttpResponse
if let Some(course) = selected_course {
HttpResponse::Ok().json(course)
} else {
HttpResponse::NotFound().json("Course not found")
}
}
程式碼解說:
params.into_inner(): 從web::Path提取元組(i32, i32)。.find(...): 在從Mutex中安全地複製出來的課程列表上,尋找符合tutor_id和course_id的課程。if let Some(course) ...: 這種寫法清晰地處理了「找到」與「沒找到」兩種情況,並分別回傳 200 OK 或 404 Not Found 的回應。
再次執行 cargo test,所有測試現在都應該通過了,我們達成了 TDD 的「綠燈」階段。
圖表解說:單元測試工作流程
此循序圖詳細展示了單元測試函式如何模擬 Actix-web 的環境,直接調用並驗證 handler 的邏輯。
@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title Handler 單元測試流程
participant "Test Function" as Test
participant "web::Data" as WebData
participant "web::Path" as WebPath
participant "Handler" as Handler
participant "HttpResponse" as Response
Test -> WebData : new(AppState)
note right: 準備共享狀態
Test -> WebPath : from((1, 1))
note right: 準備路徑參數
Test -> Handler : 呼叫 get_course_detail(app_state, params)
Handler -> Handler : 執行業務邏輯\n(lock, find, etc.)
Handler -> Response : 建立 HttpResponse (Ok or NotFound)
Handler --> Test : 回傳 HttpResponse
Test -> Response : .status()
Response --> Test : 回傳 StatusCode
Test -> Test : assert_eq!(status, StatusCode::OK)
note right: 驗證結果
end note
end note
end note
@enduml
步驟四:註冊路由並執行
最後,在 main.rs 中將我們的 handler 註冊到對應的路由上。
// 在 src/routes.rs
pub fn course_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/courses")
// ... 其他課程路由
.route("/{tutor_id}/{course_id}", web::get().to(get_course_detail)),
);
}
現在,當您啟動伺服器並使用 curl -X GET localhost:3000/courses/1/1 這樣的指令時,get_course_detail handler 就會執行,並根據其內部邏輯回傳課程的 JSON 資料或 404 錯誤。透過 TDD 的實踐,我們在開發過程中就已經確保了這段邏輯的正確性與穩健性。