DEV Community

Cover image for วิธีทดสอบ Rust API
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

วิธีทดสอบ Rust API

Rust ช่วยให้คุณสร้าง HTTP เซิร์ฟเวอร์ที่รวดเร็วและปลอดภัยได้ด้วยโค้ดไม่กี่ร้อยบรรทัด แต่สิ่งที่ Rust toolchain ไม่ได้ให้โดยตรงคือวงจร feedback ที่เร็วสำหรับการทดสอบ API ที่กำลังรันอยู่จริง: cargo test อาจช้า, การเปลี่ยน trait เล็กน้อยทำให้ต้องรันใหม่หลายส่วน และการทดสอบ HTTP endpoint มักต้องเขียน integration test แยกก่อนจะเห็นผลลัพธ์จริง หากเป้าหมายของคุณคือส่งมอบ API contract ไม่ใช่แค่ binary คุณควรมีเครื่องมือที่เรียกเซิร์ฟเวอร์ Rust ผ่าน HTTP ได้โดยตรง

ลองใช้ Apidog วันนี้

คู่มือนี้แสดง 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:

  1. แยก contract test ออกจาก build process

    คุณไม่ต้องรอ rustc ทุกครั้งเพื่อเช็กว่า endpoint ยังตอบ 200 และ shape ของ JSON ไม่เปลี่ยน

  2. แชร์ mock ได้

    Frontend ใช้ mock URL เดียวกันกับที่ backend ตกลงไว้ ไม่ต้องรอ handler compile เสร็จ

  3. สร้าง 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();
}
Enter fullscreen mode Exit fullscreen mode

รัน server:

cargo run
Enter fullscreen mode Exit fullscreen mode

จากนั้นเปิด 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
Enter fullscreen mode Exit fullscreen mode

บันทึก 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();
Enter fullscreen mode Exit fullscreen mode

แทนที่จะ 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));
Enter fullscreen mode Exit fullscreen mode

ใน Apidog สร้าง request:

  • Method: POST
  • URL: {{baseUrl}}/users
  • Body: JSON
{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
Enter fullscreen mode Exit fullscreen mode

ส่ง 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(/^[^@]+@[^@]+$/);
});
Enter fullscreen mode Exit fullscreen mode

ประโยชน์คือ test นี้ตรวจ shape จริงที่ client เห็น ไม่ใช่ Rust type ภายในเท่านั้น หากภายหลังมีการเปลี่ยน Serde attribute เช่น:

#[serde(rename_all = "camelCase")]
Enter fullscreen mode Exit fullscreen mode

แล้ว 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");
});
Enter fullscreen mode Exit fullscreen mode

ถ้าคุณเปิดใช้ 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(),
    }))
}
Enter fullscreen mode Exit fullscreen mode

ใน 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);
Enter fullscreen mode Exit fullscreen mode

จากนั้นตั้งค่า 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
Enter fullscreen mode Exit fullscreen mode

รูปแบบ frame ทั่วไป:

data: { ... }

Enter fullscreen mode Exit fullscreen mode

ใน 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 จริง

ตัวอย่าง:

  1. คลิกขวา request create-user
  2. เลือก Smart Mock
  3. เปิดใช้งาน mock

Apidog จะสร้าง mock URL เช่น:

https://mock.apidog.com/m1/<projectId>/users
Enter fullscreen mode Exit fullscreen mode

Frontend สามารถ POST ไปยัง mock URL นี้ด้วย body เดียวกัน:

{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
Enter fullscreen mode Exit fullscreen mode

ถ้าต้องการ mock แบบ dynamic ให้ใช้ Advanced Mock script:

return {
  id: Math.floor(Math.random() * 10000),
  name: body.name,
  email: body.email,
  createdAt: new Date().toISOString()
};
Enter fullscreen mode Exit fullscreen mode

เมื่อ Rust handler พร้อมใช้งาน frontend เพียงเปลี่ยน base URL กลับเป็น:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

contract ไม่ต้องเปลี่ยน

แนวคิดเดียวกันนี้ใช้ได้กับ runtime อื่นด้วย เช่น การสร้างและทดสอบ Spring Boot API และ ขั้นตอนการทดสอบ API ทั่วไป

ขั้นตอนที่ 8: บันทึกเป็น CI test scenario

เมื่อมี request หลักครบแล้ว ให้รวมเป็น Test Scenario เพื่อรันแบบ headless

ตัวอย่าง scenario:

  1. health-check

    ตรวจ 200

  2. create-user

    ตรวจ 200 และเก็บ body.id เป็นตัวแปร

  3. create-user-missing-email

    ตรวจ 422

  4. me

    ใช้ JWT จาก Pre-Request Script แล้วตรวจ 200

  5. SSE request

    ตรวจว่า stream จบภายใน 5 วินาที

จากนั้น export scenario เป็น JSON แล้ว commit ไว้ใน repo เช่น:

tests/apidog/contract.json
Enter fullscreen mode Exit fullscreen mode

ตัวอย่าง 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"
Enter fullscreen mode Exit fullscreen mode

ผลลัพธ์คือทุก 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
Enter fullscreen mode Exit fullscreen mode

ตัวอย่างแนวทาง:

apidog-cli export --format openapi --output openapi.json
Enter fullscreen mode Exit fullscreen mode

จากนั้น 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
Enter fullscreen mode Exit fullscreen mode

เพื่อให้ 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
Enter fullscreen mode Exit fullscreen mode

ถ้าใช้ 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)