5.2 測試策略:從單元測試到端對端測試#
測試金字塔與成本模型#
\[ \text{測試成本} \propto \text{測試層級高度}, \quad \text{信心} \propto \text{測試層級高度} \]
/ E2E \ 少而貴:全流程,最接近真實
/ 整合測試 \ 中:多模組協作(API + DB)
/ 單元測試 \ 多而廉:純函數,快如閃電
測試策略的核心是把業務邏輯放在低層可測的位置 —— 架構與可測性互為因果:分層清晰的系統,純函數自然多。
Rust 端:測試即公民#
單元測試(與程式碼同檔)#
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slug_title_trims_whitespace() {
assert_eq!(slug(" hello "), "hello");
}
#[test]
fn empty_title_is_invalid() {
assert!(CreateTodo { title: "".into() }.validate().is_err());
}
}
cargo test 內建執行器,無需框架 —— 官方工具鏈統一的又一次體現。
整合測試(tests/ 目錄 + 真實資料庫)#
// tests/api.rs
#[tokio::test]
async fn create_then_toggle_todo() {
let app = test_app().await; // 測試專用 DB,隨機 schema
let token = app.login("a@b.c", "pw123456").await;
let todo: Todo = app.post("/todos", &json!({"title": "write tests"}), &token).await;
assert!(!todo.done);
let toggled: Todo = app.patch(&format!("/todos/{}", todo.id), &token).await;
assert!(toggled.done);
}
關鍵實務:每個測試用獨立 schema/交易回滾,測試可並行且互不污染。
Property-Based Testing:從例子到性質#
use proptest::prelude::*;
proptest! {
// 性質:對任意標題,slug 永不含空白且不超長
#[test]
fn slug_never_has_spaces(s in "[a-zA-Z0-9 ]{0,200}") {
let out = slug(&s);
prop_assert!(!out.contains(' '));
prop_assert!(out.len() <= 200);
}
}
範例測試驗證「這幾個輸入對」;性質測試驗證不變式對所有輸入成立 —— 從歸納到演繹的信心躍升,特別適合解析器、序列化往返等高度不變的領域。
序列化往返性質#
proptest! {
fn todo_json_roundtrip(t in todo_strategy()) {
let json = serde_json::to_string(&t).unwrap();
let back: Todo = serde_json::from_str(&json).unwrap();
prop_assert_eq!(t, back); // 序列化必須是等價變換
}
}
這直接守護 4.4 節的型別管道 —— 序列化語意一改,CI 立刻爆。
TypeScript 端#
元件測試(React Native Testing Library)#
it('renders done marker when todo.done', () => {
render(<TodoItem todo={{ id: '1', title: 'x', done: true }} onToggle={jest.fn()} />);
expect(screen.getByText(/✓/)).toBeTruthy();
});
驗證層測試#
it('rejects malformed todo from server', () => {
expect(() => TodoSchema.parse({ id: 'not-uuid' })).toThrow();
});
E2E:Maestro(Expo 官方推薦)#
# .maestro/login.yaml
appId: com.example.myapp
---
- launchApp
- tapOn: "Email"
- inputText: "a@b.c"
- tapOn: "Password"
- inputText: "pw123456"
- tapOn: "登入"
- assertVisible: "我的待辦"
E2E 數量要克制:只覆蓋關鍵路徑(登入、核心 CRUD)。維護成本與脆弱性隨數量超線性成長。
CI 中的測試矩陣#
# .github/workflows/ci.yml(節選)
jobs:
backend:
runs-on: ubuntu-latest
services:
postgres: { image: postgres:16, env: { POSTGRES_PASSWORD: test } }
steps:
- uses: actions/checkout@v4
- run: cargo fmt --check && cargo clippy -- -D warnings
- run: cargo test
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx tsc --noEmit # 型別檢查本身也是測試!
- run: npm test
tsc --noEmit 是最便宜的一批「測試」 —— 它驗證的是型別契約,零維護成本。這再次印證本書主題:型別系統是最便宜的自動化測試。
習題#
- 把 5.1 的
toggle端點寫成三個整合測試:成功、不存在、他人的 todo。 - 設計一個序列化往返的 property test,覆蓋
Option<NaiveDate>。 - 為什麼 E2E 只測關鍵路徑?用成本模型論證。