ข้ามไปยังเนื้อหา

การทดสอบ Handlers

ใน Node.js คุณใช้ supertest ยิง HTTP request เข้า Express/Nest app แบบ in-process โดยไม่ต้องเปิด server จริง ฝั่ง Axum ของเทียบเท่าคือ tower::ServiceExt::oneshot — ขับ router ในฐานะ Tower service ส่ง Request เข้าไปหนึ่งใบแล้วรอ Response จบใน process เดียว ไม่มี socket

TypeScript
// Jest + supertest (NestJS)
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { AppModule } from '../src/app.module';
describe('GET /users', () => {
let app;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('returns 200 with users array', async () => {
const res = await request(app.getHttpServer()).get('/users');
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
afterAll(() => app.close());
});
Rust
use axum::{
body::Body,
http::{Request, StatusCode},
routing::get,
Router,
};
use tower::ServiceExt; // provides .oneshot()
use serde_json::Value;
fn build_app() -> Router {
Router::new().route("/users", get(list_users))
}
#[tokio::test]
async fn test_list_users_returns_200() {
let app = build_app();
let response = app
.oneshot(
Request::builder()
.uri("/users")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: Value = serde_json::from_slice(&body).unwrap();
assert!(json.is_array());
}
use axum::{
body::Body,
http::{header, Method, Request, StatusCode},
};
use serde_json::json;
use tower::ServiceExt;
#[tokio::test]
async fn test_create_user_returns_201() {
let app = build_app_with_state(test_state().await);
let body = json!({ "name": "Alice", "email": "[email protected]" });
let response = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/users")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let user: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(user["name"], "Alice");
}
#[tokio::test]
async fn test_get_nonexistent_user_returns_404() {
let app = build_app();
let response = app
.oneshot(
Request::builder()
.uri("/users/00000000-0000-0000-0000-000000000000")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}

สำหรับ tests ที่ต้องการ database ให้ inject test pool ที่ชี้ไปยัง test database:

async fn test_state() -> Arc<AppState> {
let pool = PgPool::connect(
&std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://localhost/myapp_test".to_string()),
)
.await
.unwrap();
sqlx::migrate!().run(&pool).await.unwrap();
Arc::new(AppState { db: pool })
}
fn build_app_with_state(state: Arc<AppState>) -> Router {
Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).delete(delete_user))
.with_state(state)
}

รันบนเครื่องของคุณ#[tokio::test] tests ต้องการ Tokio runtime รัน cargo test ในไดเรกทอรีโปรเจกต์ของคุณ สำหรับ database tests ตั้งค่า TEST_DATABASE_URL และรัน cargo test -- --test-threads=1 เพื่อหลีกเลี่ยง concurrent migration conflicts

`tower::ServiceExt::oneshot` ทำอะไร?
attribute ใดที่ mark async Axum handler test?
`tower` (สำหรับ `ServiceExt`) ควรประกาศที่ใดใน `Cargo.toml`?