Rust ช่วยให้คุณสร้าง HTTP เซิร์ฟเวอร์ที่รวดเร็วและปลอดภัยได้ด้วยโค้ดไม่กี่ร้อยบรรทัด แต่สิ่งที่ Rust toolchain ไม่ได้ให้โดยตรงคือวงจร feedback ที่เร็วสำหรับการทดสอบ API ที่กำลังรันอยู่จริง: cargo test อาจช้า, การเปลี่ยน trait เล็กน้อยทำให้ต้องรันใหม่หลายส่วน และการทดสอบ HTTP endpoint มักต้องเขียน integration test แยกก่อนจะเห็นผลลัพธ์จริง หากเป้าหมายของคุณคือส่งมอบ API contract ไม่ใช่แค่ binary คุณควรมีเครื่องมือที่เรียกเซิร์ฟเวอร์ Rust ผ่าน HTTP ได้โดยตรง
คู่มือนี้แสดง workflow สำหรับทดสอบ Rust API ด้วย Apidog: เชื่อมต่อกับ Axum หรือ Actix server, สร้าง request สำหรับ endpoint, ตรวจสอบ JSON ที่ serialize/deserialize ด้วย Serde, จัดการ JWT, mock endpoint ที่ยังพัฒนาไม่เสร็จ และนำ test scenario ไปรันใน CI เพื่อจับ contract drift ก่อน merge
หากคุณเคยใช้ Postman หรือ curl มาก่อน Apidog จะเพิ่ม workflow แบบ design-first ให้ เช่น OpenAPI spec จาก request ที่บันทึกไว้, mock URL ที่แชร์ให้ frontend ได้ และ environment สำหรับทีม ส่วนเรื่อง การย้ายจาก Postman อ่านแยกได้; บทความนี้จะโฟกัส Rust
สรุปย่อ
- รัน Rust server ในเครื่อง เช่น
cargo runกับ Axum หรือ Actix-web - ตั้งค่า Apidog environment ด้วย
baseUrl = http://localhost:3000 - สร้าง request แรกสำหรับ
GET /healthzเพื่อยืนยันว่า server และ environment ทำงาน - ใช้ Serde struct เป็น reference สำหรับ JSON request/response แล้วเขียน Tests assertion ใน Apidog
- สร้าง JWT ครั้งเดียวใน Pre-Request Script แล้วเก็บเป็น
{{token}} - ใช้ folder-level Bearer Auth เพื่อให้ request ที่ป้องกันด้วย JWT ใช้ token ร่วมกัน
- Mock endpoint ที่ Rust handler ยังไม่พร้อม เพื่อให้ frontend ทำงานต่อได้
- บันทึก request เป็น Test Scenario แล้วรันใน CI ด้วย
apidog-cli
ทำไมต้องทดสอบ Rust API นอก Rust toolchain
cargo test เหมาะกับ unit test และ integration test ฝั่ง Rust แต่ไม่ใช่วิธีที่สะดวกที่สุดในการตรวจ public HTTP contract เช่น:
- status code ถูกต้องหรือไม่
- response JSON field ตรงกับที่ client ใช้หรือไม่
- header ถูกต้องหรือไม่
- error response เมื่อ payload ผิด schema เป็นแบบที่คาดหวังหรือไม่
- JWT middleware ส่ง
401ตามที่ตกลงไว้หรือไม่
ถ้าใช้ Rust ล้วน คุณอาจต้องเขียน tower::ServiceExt::oneshot หรือ test harness แยกหลายชุด และ frontend ยังต้องมี mock อีกชุดหนึ่งอยู่ดี
Apidog ทำหน้าที่เป็น contract layer ที่เรียก server ที่รันอยู่จริงผ่าน HTTP:
แยก contract test ออกจาก build process
คุณไม่ต้องรอrustcทุกครั้งเพื่อเช็กว่า endpoint ยังตอบ200และ shape ของ JSON ไม่เปลี่ยนแชร์ mock ได้
Frontend ใช้ mock URL เดียวกันกับที่ backend ตกลงไว้ ไม่ต้องรอ handler compile เสร็จสร้าง OpenAPI ได้จาก request ที่บันทึกไว้
เมื่อ request เสถียรแล้ว คุณ export เป็น OpenAPI 3.1 เพื่อให้ทีมสร้าง typed client ได้ โดยไม่ต้องเขียน spec ด้วยมือทั้งหมด
ขั้นตอนที่ 1: เพิ่ม Rust server เป็น Apidog environment
เริ่มจาก Axum server แบบง่าย:
use axum::{routing::get, Router};
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let app = Router::new().route("/healthz", get(|| async { "ok" }));
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
รัน server:
cargo run
จากนั้นเปิด Apidog และสร้างโปรเจกต์ใหม่ แล้วเพิ่ม environment ชื่อ Rust Local
| ตัวแปร | ค่า |
|---|---|
baseUrl |
http://localhost:3000 |
token |
เว้นว่างไว้ก่อน |
apiVersion |
v1 |
แนะนำให้เพิ่ม environment อีกชุด เช่น Rust Staging สำหรับ staging URL จริง เพื่อให้สลับ environment ได้จาก dropdown โดยไม่ต้องแก้ URL ในทุก request
ขั้นตอนที่ 2: เรียก endpoint แรก
สร้างโฟลเดอร์ชื่อ Rust API แล้วสร้าง request ใหม่:
- Method:
GET - URL:
{{baseUrl}}/healthz
กด Send
ถ้า server ทำงานอยู่ คุณควรได้:
Status: 200
Body: ok
บันทึก request นี้เป็น health-check
นี่คือ smoke test ที่ควรมีในทุก API project เพราะช่วยยืนยันสามอย่างพร้อมกัน:
- Apidog เรียก server ได้
-
baseUrlถูกต้อง - Rust process กำลัง listen อยู่ที่ port ที่คาดไว้
ถ้าเจอ connection refused ให้ตรวจสอบว่า server bind ถูก host และ port แล้วหรือยัง สำหรับ local development แนะนำ:
TcpListener::bind("0.0.0.0:3000").await.unwrap();
แทนที่จะ bind เฉพาะ 127.0.0.1 ในบาง environment โดยเฉพาะเมื่อเกี่ยวข้องกับ Docker หรือ network interface อื่น
ขั้นตอนที่ 3: ทดสอบ JSON request/response ด้วย Serde
ตัวอย่าง endpoint แบบ JSON-in, JSON-out:
use axum::{extract::Json, routing::post, Router};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
Json(User {
id: 1,
name: payload.name,
email: payload.email,
})
}
let app = Router::new().route("/users", post(create_user));
ใน Apidog สร้าง request:
- Method:
POST - URL:
{{baseUrl}}/users - Body: JSON
{
"name": "Ada Lovelace",
"email": "ada@example.com"
}
ส่ง request แล้วบันทึกเป็น create-user
จากนั้นเปิดแท็บ Tests แล้วเพิ่ม assertion:
pm.test("Status is 200", () => {
pm.expect(pm.response.code).to.eql(200);
});
pm.test("Body has id, name, email", () => {
const body = pm.response.json();
pm.expect(body).to.have.property("id");
pm.expect(body.name).to.eql("Ada Lovelace");
pm.expect(body.email).to.match(/^[^@]+@[^@]+$/);
});
ประโยชน์คือ test นี้ตรวจ shape จริงที่ client เห็น ไม่ใช่ Rust type ภายในเท่านั้น หากภายหลังมีการเปลี่ยน Serde attribute เช่น:
#[serde(rename_all = "camelCase")]
แล้ว response field เปลี่ยน public contract, Apidog test จะล้มเหลวทันที
ขั้นตอนที่ 4: ครอบคลุมกรณีที่ Serde ปฏิเสธ payload
ทดสอบเฉพาะ happy path ไม่พอ คุณควรบันทึก request สำหรับ invalid payload ด้วย เพื่อระบุ validation contract ให้ชัดเจน
| Request | Body | คาดหวัง |
|---|---|---|
create-user-missing-email |
{ "name": "Ada" } |
422, body กล่าวถึง missing field email
|
create-user-extra-field |
{ "name": "Ada", "email": "a@b.c", "admin": true } |
200 ถ้าไม่มี #[serde(deny_unknown_fields)]; มิฉะนั้น 422
|
create-user-wrong-type |
{ "name": 1, "email": "a@b.c" } |
422, กล่าวถึง invalid type: integer
|
ตัวอย่าง assertion สำหรับ missing email:
pm.test("Missing email returns 422", () => {
pm.expect(pm.response.code).to.eql(422);
});
pm.test("Error mentions missing email", () => {
pm.expect(pm.response.text()).to.include("email");
});
ถ้าคุณเปิดใช้ deny_unknown_fields ในอนาคต request create-user-extra-field จะเปลี่ยนจากผ่านเป็นล้มเหลว นั่นคือสัญญาณว่า public API contract เปลี่ยนแล้ว และควรสื่อสารกับ client ก่อน merge
ขั้นตอนที่ 5: ทดสอบ route ที่ป้องกันด้วย JWT
Rust API ที่ใช้งานจริงมักมี auth middleware ตัวอย่าง pattern ใน Axum:
use axum::{extract::Json, http::StatusCode};
use axum_extra::extract::cookie::PrivateCookieJar;
use jsonwebtoken::{decode, DecodingKey, Validation};
async fn me(jar: PrivateCookieJar) -> Result<Json<User>, StatusCode> {
let token = jar.get("token").ok_or(StatusCode::UNAUTHORIZED)?;
let claims = decode::<Claims>(
token.value(),
&DecodingKey::from_secret(b"secret"),
&Validation::default(),
)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(Json(User {
id: claims.claims.sub,
name: "Ada".into(),
email: "ada@example.com".into(),
}))
}
ใน Apidog ไม่ต้องสร้าง JWT ด้วยมือทุกครั้ง ให้เพิ่ม Pre-Request Script ที่ระดับ folder:
const jwt = require("jsonwebtoken");
const token = jwt.sign(
{
sub: 1,
exp: Math.floor(Date.now() / 1000) + 3600
},
"secret"
);
pm.environment.set("token", token);
จากนั้นตั้งค่า folder auth:
- Auth Type: Bearer Token
- Token:
{{token}}
ทุก request ใน folder จะใช้ token ล่าสุดโดยอัตโนมัติ ลดปัญหา test fail เพราะ token หมดอายุ
อ่านเพิ่มเติมได้ที่ วิธีทดสอบการยืนยันตัวตนด้วย JWT ใน API
ขั้นตอนที่ 6: ทดสอบ streaming และ Server-Sent Events
Axum รองรับ streaming และ Server-Sent Events ผ่าน Sse ซึ่งส่ง response เป็น:
Content-Type: text/event-stream
รูปแบบ frame ทั่วไป:
data: { ... }
ใน Apidog ให้สร้าง GET request ไปยัง SSE endpoint ตามปกติ เมื่อ response เป็น text/event-stream แผง response จะเข้าสู่ streaming mode และแสดง frame ที่เข้ามาตามเวลา
สิ่งที่ควรตรวจ:
- chunk แรกมาถึงภายใน SLA ที่กำหนด
- มี event ปิดท้าย เช่น
event: done - stream จบภายในเวลาที่กำหนด
- handler ไม่ loop ตลอดไปโดยไม่ได้ตั้งใจ
ตั้งค่า request timeout เพื่อให้ test fail หาก stream ไม่จบ เช่น ภายใน 5 วินาที
ถ้า endpoint ใช้ WebSocket แทน SSE ให้ใช้ request type แบบ WebSocket ใน Apidog แล้วบันทึกลำดับ message และ assertion ในลักษณะเดียวกัน
ขั้นตอนที่ 7: Mock Rust API สำหรับ frontend development
Frontend ไม่ควรถูก block เพราะ Rust handler ยังไม่เสร็จ Apidog mock ช่วยให้คุณแชร์ URL ที่ตอบตาม contract เดียวกับ backend ได้ก่อน implementation จริง
ตัวอย่าง:
- คลิกขวา request
create-user - เลือก Smart Mock
- เปิดใช้งาน mock
Apidog จะสร้าง mock URL เช่น:
https://mock.apidog.com/m1/<projectId>/users
Frontend สามารถ POST ไปยัง mock URL นี้ด้วย body เดียวกัน:
{
"name": "Ada Lovelace",
"email": "ada@example.com"
}
ถ้าต้องการ mock แบบ dynamic ให้ใช้ Advanced Mock script:
return {
id: Math.floor(Math.random() * 10000),
name: body.name,
email: body.email,
createdAt: new Date().toISOString()
};
เมื่อ Rust handler พร้อมใช้งาน frontend เพียงเปลี่ยน base URL กลับเป็น:
http://localhost:3000
contract ไม่ต้องเปลี่ยน
แนวคิดเดียวกันนี้ใช้ได้กับ runtime อื่นด้วย เช่น การสร้างและทดสอบ Spring Boot API และ ขั้นตอนการทดสอบ API ทั่วไป
ขั้นตอนที่ 8: บันทึกเป็น CI test scenario
เมื่อมี request หลักครบแล้ว ให้รวมเป็น Test Scenario เพื่อรันแบบ headless
ตัวอย่าง scenario:
health-check
ตรวจ200create-user
ตรวจ200และเก็บbody.idเป็นตัวแปรcreate-user-missing-email
ตรวจ422me
ใช้ JWT จาก Pre-Request Script แล้วตรวจ200SSE request
ตรวจว่า stream จบภายใน 5 วินาที
จากนั้น export scenario เป็น JSON แล้ว commit ไว้ใน repo เช่น:
tests/apidog/contract.json
ตัวอย่าง GitHub Actions step:
- name: Run API contract tests
run: |
cargo build --release
./target/release/myserver &
sleep 2
apidog-cli run tests/apidog/contract.json --env "Rust Local"
ผลลัพธ์คือทุก PR ที่แตะ handler จะถูกตรวจด้วย binary จริง หากมีการเปลี่ยนชื่อ field จาก Serde, เปลี่ยน status code, หรือแก้ JWT middleware จน public contract พัง CI จะหยุดก่อน merge
ขั้นตอนที่ 9: สร้าง OpenAPI จาก request ที่บันทึกไว้
เมื่อ request และ assertion เสถียรแล้ว ให้ export OpenAPI 3.1 จาก Apidog
ผลลัพธ์คือ spec ที่อ้างอิงจาก request/response ตัวอย่างที่ใช้ทดสอบจริง เหมาะสำหรับทีมที่ต้องสร้าง client เช่น:
- TypeScript
- Swift
- Kotlin
- Python
ถ้าต้องการเก็บ spec ไว้ใน Rust repo ให้รัน export จาก CI แล้วเขียนออกเป็น:
openapi.json
ตัวอย่างแนวทาง:
apidog-cli export --format openapi --output openapi.json
จากนั้น client generator หรือ documentation pipeline สามารถใช้ไฟล์เดียวกันได้
คำถามที่พบบ่อย
Apidog ใช้ได้ทั้ง Axum และ Actix-web หรือไม่?
ได้ Apidog สื่อสารผ่าน HTTP ไม่ได้ผูกกับ framework ฝั่ง Rust ดังนั้นใช้ได้กับ Axum, Actix-web, Rocket, Warp, Poem, Loco หรือ framework อื่นที่ตอบ HTTP request ได้
ข้อควรระวังคือ local binding แนะนำให้ใช้:
0.0.0.0:3000
เพื่อให้ tool และ container อื่นเข้าถึงได้ง่ายกว่า 127.0.0.1
ทดสอบ handler ที่ panic ได้อย่างไร?
ใช้ CatchPanicLayer จาก tower-http หน้า router เพื่อเปลี่ยน panic เป็น 500 response แล้วสร้าง Apidog request ที่ trigger route นั้น
ถ้าไม่ catch panic connection อาจหลุดและ Apidog จะรายงาน network error ซึ่งยังถือเป็น contract signal ได้เช่นกัน หาก client คาดหวัง JSON error แต่ได้ connection reset แปลว่า contract ไม่ตรง
รัน Apidog กับ Rust binary ใน Docker ได้ไหม?
ได้ ตั้ง baseUrl ไปยัง exposed port ของ container เช่น:
http://localhost:3000
ถ้าใช้ Docker Compose ให้แน่ใจว่า Apidog runner อยู่ network เดียวกัน หรือใช้ host-mapped port ที่เข้าถึงได้จาก CI runner
แล้ว gRPC ล่ะ?
Apidog รองรับ gRPC request โดยนำเข้า .proto, เลือก service/method, ใส่ payload แล้วส่ง request ได้ รูปแบบ environment, auth และ test scenario ใช้แนวคิดเดียวกับ REST
Test scenario แทนที่ cargo test ได้ไหม?
ไม่ควรแทนที่กัน
-
cargo testตรวจ logic ภายใน Rust code - Apidog ตรวจ public HTTP contract ที่ client ใช้งานจริง
คุณควรมีทั้งสองระดับ เพราะจับ bug คนละประเภท เช่น unit test จับ business logic ที่ผิด ส่วน Apidog จับ response shape, CORS header, status code หรือ auth behavior ที่เปลี่ยนไป
Apidog ฟรีสำหรับ Rust open-source project หรือไม่?
Apidog มี free tier สำหรับบุคคลและทีมขนาดเล็ก รวมถึง test scenarios, mocks และ OpenAPI export หากคุณดูแล public Rust API คุณสามารถ commit project/test artifacts ไว้ใน repo เพื่อให้ contributor รัน contract test ได้ง่ายขึ้น
สรุป
Rust API ควรมี feedback loop ที่ไม่ต้องรอ compiler ทุกครั้ง Apidog ช่วยให้คุณทดสอบ API ที่รันอยู่จริงผ่าน HTTP, เขียน assertion ข้าง request, สร้าง mock ให้ frontend และรัน contract test ใน CI ได้
เริ่มจาก GET /healthz, เพิ่ม JSON endpoint, ครอบคลุม invalid payload, ตั้ง JWT, mock endpoint ที่ยังไม่เสร็จ แล้วรวมทั้งหมดเป็น test scenario เมื่อ contract เปลี่ยน คุณจะเห็น fail ในขั้นตอนที่ควบคุมได้ ไม่ใช่เจอจาก client หลัง deploy
ดาวน์โหลด Apidog แล้วเชื่อมต่อกับ Rust server ของคุณ การตั้งค่าใช้เวลาไม่นาน แต่ช่วยให้ backend และ frontend ทำงานบน API contract เดียวกันได้ตั้งแต่ต้น
Top comments (0)