การทดสอบ Handlers
supertest/jest vs tower::ServiceExt::oneshot
หัวข้อที่มีชื่อว่า “supertest/jest vs tower::ServiceExt::oneshot”ใน Node.js คุณใช้ supertest ยิง HTTP request เข้า Express/Nest app แบบ in-process โดยไม่ต้องเปิด server จริง ฝั่ง Axum ของเทียบเท่าคือ tower::ServiceExt::oneshot — ขับ router ในฐานะ Tower service ส่ง Request เข้าไปหนึ่งใบแล้วรอ Response จบใน process เดียว ไม่มี socket
// 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());});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());}ทดสอบ POST endpoint ด้วย JSON body
หัวข้อที่มีชื่อว่า “ทดสอบ POST endpoint ด้วย JSON body”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 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");}ทดสอบ error paths
หัวข้อที่มีชื่อว่า “ทดสอบ error paths”#[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);}Test helper: สร้าง app ด้วย mock state
หัวข้อที่มีชื่อว่า “Test helper: สร้าง app ด้วย mock state”สำหรับ 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