Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/testing-guide/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "testing-guide"
version = "0.1.0"
edition = "2021"

[dependencies]
gotcha = { path = "../../gotcha", features = ["openapi"] }
gotcha_macro = { path = "../../gotcha_macro" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
axum = "0.7"
uuid = { version = "1", features = ["serde", "v4"] }

[dev-dependencies]
axum-test = "16"
tower = "0.5"
http = "1"
assert-json-diff = "2"
uuid = { version = "1", features = ["serde", "v4"] }
54 changes: 54 additions & 0 deletions examples/testing-guide/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Gotcha 框架测试示例

展示如何为 Gotcha 应用编写集成测试。

## 核心区别

与 Axum 相比,Gotcha 的主要区别:

1. **Handler 状态类型**
```rust
// Gotcha 使用
State(ctx): State<GotchaContext<AppState, AppConfig>>

// 而不是 Axum 的
State(state): State<AppState>
```

2. **错误处理返回 HTTP 状态码**
```rust
// 返回正确的状态码
Result<Json<T>, (StatusCode, Json<ErrorResponse>)>
```

3. **测试设置**
```rust
pub async fn create_test_app() -> axum::Router {
let app = App;
let config = ConfigWrapper { /* ... */ };
let state = app.state(&config).await.unwrap();
let context = GotchaContext { state, config };
app.build_router(context).await.unwrap()
}
```

## 运行测试

```bash
cargo test --test api_integration_test
```

## 文件结构

- `src/lib.rs` - 示例 CRUD API 实现
- `tests/api_integration_test.rs` - 7 个集成测试场景
- `tests/integration_tests.rs` - 另一组集成测试(可选)

## 依赖

```toml
[dev-dependencies]
axum-test = "16"
```

就这样,其他跟 Axum 测试一样。
284 changes: 284 additions & 0 deletions examples/testing-guide/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// Business application code using Gotcha framework

use gotcha::{
api, ConfigWrapper, GotchaApp, GotchaContext, GotchaRouter,
Json, Path, Query, State, Schematic
};
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use uuid::Uuid;

// ========== Application State ==========
#[derive(Clone)]
pub struct AppState {
pub users: Arc<Mutex<Vec<User>>>,
pub counter: Arc<Mutex<i32>>,
}

impl Default for AppState {
fn default() -> Self {
Self {
users: Arc::new(Mutex::new(vec![
User {
id: Uuid::new_v4(),
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
age: 30,
}
])),
counter: Arc::new(Mutex::new(0)),
}
}
}

// ========== Data Models ==========
#[derive(Clone, Debug, Serialize, Deserialize, Schematic, PartialEq)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub age: u32,
}

#[derive(Clone, Debug, Serialize, Deserialize, Schematic)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
pub age: u32,
}

#[derive(Clone, Debug, Serialize, Deserialize, Schematic)]
pub struct UpdateUserRequest {
pub name: Option<String>,
pub email: Option<String>,
pub age: Option<u32>,
}

#[derive(Clone, Debug, Serialize, Deserialize, Schematic)]
pub struct QueryParams {
pub page: Option<usize>,
pub size: Option<usize>,
pub sort: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, Schematic, PartialEq)]
pub struct ApiResponse<T: Schematic> {
pub success: bool,
pub message: String,
pub data: Option<T>,
}

#[derive(Clone, Debug, Serialize, Deserialize, Schematic)]
pub struct ErrorResponse {
pub error: String,
pub code: u16,
}

// ========== Handler Functions ==========

#[api(id = "health_check", group = "system")]
pub async fn health_check(State(ctx): State<GotchaContext<AppState, AppConfig>>) -> Json<ApiResponse<String>> {
let counter = ctx.state.counter.lock().unwrap();
Json(ApiResponse {
success: true,
message: "Service is healthy".to_string(),
data: Some(format!("Counter: {}", *counter)),
})
}

#[api(id = "list_users", group = "users")]
pub async fn list_users(
State(ctx): State<GotchaContext<AppState, AppConfig>>,
Query(params): Query<QueryParams>,
) -> Json<ApiResponse<Vec<User>>> {
let users = ctx.state.users.lock().unwrap();
let page = params.page.unwrap_or(1);
let size = params.size.unwrap_or(10);

let start = (page - 1) * size;
let end = std::cmp::min(start + size, users.len());

let paginated_users = if start < users.len() {
users[start..end].to_vec()
} else {
vec![]
};

Json(ApiResponse {
success: true,
message: format!("Found {} users", paginated_users.len()),
data: Some(paginated_users),
})
}

#[api(id = "get_user", group = "users")]
pub async fn get_user(
State(ctx): State<GotchaContext<AppState, AppConfig>>,
Path(user_id): Path<Uuid>,
) -> Result<Json<ApiResponse<User>>, (StatusCode, Json<ErrorResponse>)> {
let users = ctx.state.users.lock().unwrap();

match users.iter().find(|u| u.id == user_id) {
Some(user) => Ok(Json(ApiResponse {
success: true,
message: "User found".to_string(),
data: Some(user.clone()),
})),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "User not found".to_string(),
code: 404,
}),
)),
}
}

#[api(id = "create_user", group = "users")]
pub async fn create_user(
State(ctx): State<GotchaContext<AppState, AppConfig>>,
Json(payload): Json<CreateUserRequest>,
) -> Json<ApiResponse<User>> {
let mut users = ctx.state.users.lock().unwrap();
let mut counter = ctx.state.counter.lock().unwrap();

let new_user = User {
id: Uuid::new_v4(),
name: payload.name,
email: payload.email,
age: payload.age,
};

users.push(new_user.clone());
*counter += 1;

Json(ApiResponse {
success: true,
message: "User created successfully".to_string(),
data: Some(new_user),
})
}

#[api(id = "update_user", group = "users")]
pub async fn update_user(
State(ctx): State<GotchaContext<AppState, AppConfig>>,
Path(user_id): Path<Uuid>,
Json(payload): Json<UpdateUserRequest>,
) -> Result<Json<ApiResponse<User>>, (StatusCode, Json<ErrorResponse>)> {
let mut users = ctx.state.users.lock().unwrap();

match users.iter_mut().find(|u| u.id == user_id) {
Some(user) => {
if let Some(name) = payload.name {
user.name = name;
}
if let Some(email) = payload.email {
user.email = email;
}
if let Some(age) = payload.age {
user.age = age;
}

Ok(Json(ApiResponse {
success: true,
message: "User updated successfully".to_string(),
data: Some(user.clone()),
}))
}
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "User not found".to_string(),
code: 404,
}),
)),
}
}

#[api(id = "delete_user", group = "users")]
pub async fn delete_user(
State(ctx): State<GotchaContext<AppState, AppConfig>>,
Path(user_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, (StatusCode, Json<ErrorResponse>)> {
let mut users = ctx.state.users.lock().unwrap();

let initial_len = users.len();
users.retain(|u| u.id != user_id);

if users.len() < initial_len {
Ok(Json(ApiResponse {
success: true,
message: "User deleted successfully".to_string(),
data: Some(()),
}))
} else {
Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "User not found".to_string(),
code: 404,
}),
))
}
}

// ========== Application Configuration ==========
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub app_name: String,
pub version: String,
pub max_users: usize,
}

impl Default for AppConfig {
fn default() -> Self {
Self {
app_name: "Testing Guide".to_string(),
version: "1.0.0".to_string(),
max_users: 1000,
}
}
}

// ========== Application Setup ==========
pub struct App;

impl GotchaApp for App {
type State = AppState;
type Config = AppConfig;

async fn state(&self, _config: &ConfigWrapper<Self::Config>) -> Result<Self::State, Box<dyn std::error::Error>> {
Ok(AppState::default())
}

fn routes(&self, router: GotchaRouter<GotchaContext<Self::State, Self::Config>>)
-> GotchaRouter<GotchaContext<Self::State, Self::Config>> {
router
.get("/health", health_check)
.get("/users", list_users)
.get("/users/:id", get_user)
.post("/users", create_user)
.put("/users/:id", update_user)
.delete("/users/:id", delete_user)
}
}

// ========== Helper function for testing ==========
pub async fn create_test_app() -> axum::Router {
use gotcha::config::BasicConfig;

let app = App;
let config = ConfigWrapper {
basic: BasicConfig::default(),
application: AppConfig::default(),
};

let state = app.state(&config).await.unwrap();
let context = GotchaContext {
state,
config,
};

app.build_router(context).await.unwrap()
}
Loading
Loading