09 ส.ค. 2568·อ่าน 3 นาที

การทดสอบ Go REST handlers: httptest และการทดสอบแบบ table-driven

การทดสอบ handler ของ Go สำหรับ REST ด้วย httptest และกรณีแบบ table-driven ให้วิธีที่ทำซ้ำได้ในการตรวจสอบการยืนยันตัวตน การตรวจสอบค่า สถานะการตอบกลับ และกรณีขอบก่อนปล่อยใช้งาน

การทดสอบ Go REST handlers: httptest และการทดสอบแบบ table-driven

สิ่งที่ควรมั่นใจก่อนปล่อยใช้งาน

handler ของ REST อาจคอมไพล์ผ่านและผ่านการตรวจสอบแบบแมนนวลอย่างง่าย แต่ยังล้มเหลวใน production ได้ ส่วนใหญ่การล้มเหลวไม่ใช่ปัญหาด้านไวยากรณ์ แต่เป็นปัญหาเรื่องสัญญา: handler ยอมรับสิ่งที่ควรถูกปฏิเสธ ส่งสถานะผิด หรือรั่วรายละเอียดในข้อผิดพลาด

การทดสอบด้วยมือยังช่วยได้ แต่ก็ง่ายที่จะพลาดกรณีขอบ และการเกิด regression คุณจะลองเฉพาะเส้นทางที่สำเร็จ อาจลองแค่ข้อผิดพลาดเดียวที่เห็นได้ชัด แล้วก็ผ่านไป จากนั้นการเปลี่ยนแปลงเล็กน้อยใน validation หรือ middleware ก็อาจทำให้พฤติกรรมที่คุณถือว่าเสถียรถูกทำลายโดยเงียบ ๆ

เป้าหมายของการทดสอบ handler นั้นเรียบง่าย: ทำให้สัญญาที่ handler ให้ไว้สามารถทำซ้ำได้ ซึ่งรวมถึงกฎการยืนยันตัวตน การตรวจสอบค่า การคืนสถานะที่คาดเดาได้ และร่างข้อผิดพลาดที่ไคลเอนต์พึ่งพาได้

แพ็กเกจ httptest ของ Go เหมาะมากเพราะคุณสามารถเรียก handler โดยตรงโดยไม่ต้องเริ่มเซิร์ฟเวอร์จริง คุณสร้าง HTTP request ส่งให้ handler แล้วตรวจสอบ response body, headers และ status code การทดสอบจะเร็ว แยกส่วน และรันได้ง่ายทุก commit

ก่อนปล่อย คุณควรรู้ (ไม่ใช่หวัง) ว่า:

  • พฤติกรรมการยืนยันตัวตนสอดคล้องกันทั้งเมื่อ token ขาด หมดอายุ หรือสิทธิไม่พอ
  • ค่าที่รับเข้าถูกตรวจสอบ: ฟิลด์ที่ต้องมี ชนิด ช่วงค่า และ (ถ้าบังคับ) ฟิลด์ที่ไม่รู้จัก
  • สถานะตอบกลับตรงกับสัญญา (เช่น 401 vs 403, 400 vs 422)
  • การตอบข้อผิดพลาดปลอดภัยและสม่ำเสมอ (ไม่มี stack trace รูปแบบเหมือนเดิม)
  • เส้นทางที่ไม่สำเร็จถูกจัดการ: timeout, ความล้มเหลวของบริการภายนอก, ผลลัพธ์ว่าง

ตัวอย่างเช่น endpoint “Create ticket” อาจทำงานเมื่อส่ง JSON ถูกต้องในฐานะ admin แต่การทดสอบจะจับสิ่งที่คุณลืมลอง: token หมดอายุ ฟิลด์เพิ่มที่ไคลเอนต์ส่งโดยไม่ได้ตั้งใจ ค่าความสำคัญเป็นลบ หรือความแตกต่างระหว่าง “not found” กับ “internal error” เมื่อ dependency ล้มเหลว

กำหนดสัญญาสำหรับแต่ละ endpoint

เขียนสิ่งที่ handler สัญญาจะทำลงไปก่อนเขียนการทดสอบ สัญญาที่ชัดเจนทำให้การทดสอบมีเป้าหมายและหยุดไม่ให้กลายเป็นการเดาว่าโค้ด “ตั้งใจทำอะไร” นอกจากนี้ยังทำให้การ refactor ปลอดภัยขึ้นเพราะคุณสามารถเปลี่ยนภายในโดยไม่เปลี่ยนพฤติกรรม

เริ่มจากอินพุต ระบุให้ชัดเจนว่าค่าแต่ละตัวมาจากไหนและอะไรที่ต้องมี endpoint อาจรับ id จาก path, limit จาก query string, header Authorization และ body เป็น JSON ระบุรูปแบบที่อนุญาต ขอบเขตค่าต่ำสุด/สูงสุด ฟิลด์ที่ต้องมี และผลเมื่อสิ่งใดหายไป

จากนั้นกำหนดเอาต์พุต อย่าหยุดที่ "ส่ง JSON" ตัดสินใจว่า success เป็นอย่างไร header ไหนสำคัญ และข้อผิดพลาดมีรูปแบบอย่างไร หากไคลเอนต์พึ่งพารหัสข้อผิดพลาดคงที่และรูปร่าง JSON ที่คาดเดาได้ ให้ถือว่านั่นเป็นส่วนหนึ่งของสัญญา

เช็คลิสต์ใช้งานได้จริงคือ:

  • อินพุต: ค่าจาก path/query, header ที่จำเป็น และฟิลด์ JSON พร้อมกฎการตรวจสอบ
  • เอาต์พุต: status code, response headers, รูปร่าง JSON สำหรับ success และ error
  • ผลข้างเคียง: ข้อมูลใดถูกเปลี่ยนหรือสร้าง
  • พึ่งพา: การเรียกฐานข้อมูล บริการภายนอก เวลาในระบบ ID ที่สร้าง

ตัดสินใจด้วยด้วยว่าเทสต์ handler หยุดที่ไหน การทดสอบ handler แข็งแกร่งที่สุดที่ขอบเขต HTTP: auth, การแปลงค่า, validation, status codes และ error bodies เรื่องที่ลึกกว่านั้นให้ไปยัง integration tests: คำถามฐานข้อมูลจริง การเรียกเครือข่าย และ routing แบบเต็ม

ถ้า backend ของคุณถูกสร้างขึ้น (ตัวอย่าง AppMaster ผลิต Go handlers และ business logic) แนวทาง contract-first จะมีประโยชน์มากขึ้น คุณสามารถ regenerate โค้ดและยังยืนยันได้ว่าแต่ละ endpoint รักษาพฤติกรรมสาธารณะไว้เหมือนเดิม

ตั้ง harness พื้นฐานด้วย httptest

การทดสอบ handler ที่ดีควรรู้สึกเหมือนส่ง request จริงโดยไม่ต้องเริ่มเซิร์ฟเวอร์ ใน Go นั่นหมายถึงการสร้าง request ด้วย httptest.NewRequest, เก็บ response ด้วย httptest.NewRecorder, แล้วเรียก handler

การเรียก handler โดยตรงให้การทดสอบที่เร็วและมีเป้าหมาย เหมาะเมื่อต้องการตรวจสอบพฤติกรรมภายใน handler: การเช็ค auth, กฎ validation, status codes, และ error bodies การใช้ router ในการทดสอบมีประโยชน์เมื่อสัญญาขึ้นกับ path params, การจับ route หรือลำดับ middleware เริ่มจากการเรียกตรง ๆ แล้วเพิ่ม router เมื่อจำเป็น

Headers สำคัญกว่าที่หลายคนคิด การไม่มี Content-Type อาจเปลี่ยนวิธีที่ handler อ่าน body ตั้ง headers ที่คุณคาดหวังในทุกกรณีเพื่อให้ความล้มเหลวชี้ไปที่ logic ไม่ใช่การตั้งค่าการทดสอบ

นี่คือลวดแบบพื้นฐานที่นำกลับมาใช้ได้:

req := httptest.NewRequest(http.MethodPost, "/v1/widgets", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)
res := rec.Result()
defer res.Body.Close()

เพื่อให้การตรวจสอบผลสอดคล้องกัน ควรมี helper เล็ก ๆ ตัวเดียวเพื่ออ่านและถอดรหัส response body ในการทดสอบส่วนใหญ่ ให้ตรวจสอบ status code ก่อน (เพื่อให้ง่ายต่อการดูความล้มเหลว) แล้วตรวจ header สำคัญที่คุณสัญญาไว้ (มักเป็น Content-Type) แล้วค่อยตรวจ body

ถ้า backend ของคุณถูกสร้างขึ้น (รวมถึง backend Go ที่ AppMaster สร้าง) harness นี้ยังใช้ได้ คุณกำลังทดสอบสัญญา HTTP ที่ผู้ใช้พึ่งพา ไม่ใช่สไตล์โค้ดเบื้องหลัง

ออกแบบกรณีแบบ table-driven ให้ยังอ่านง่าย

การทดสอบแบบ table-driven ทำงานได้ดีที่สุดเมื่อแต่ละเคสอ่านเหมือนเรื่องสั้น ๆ: request ที่ส่งและสิ่งที่คาดหวังกลับมา คุณควรสแกนตารางแล้วเข้าใจความครอบคลุมโดยไม่ต้องกระโดดไปมาในไฟล์

เคสที่ดีมักมี: ชื่อชัดเจน request (method, path, headers, body) สถานะที่คาดหวัง และการตรวจสอบ response สำหรับ body JSON ให้ยืนยันเพียงฟิลด์ที่คงที่ไม่กี่ตัว (เช่นรหัสข้อผิดพลาด) แทนที่จะจับคู่สตริง JSON ทั้งหมด เว้นแต่สัญญาต้องการผลลัพธ์ที่เคร่งครัด

รูปแบบเคสเรียบง่ายที่นำกลับมาใช้ได้

เก็บ struct ของเคสให้เน้นจุดสำคัญ ใส่การตั้งค่าที่เฉพาะใน helper เพื่อให้ตารางยังเล็ก

type tc struct {
	name       string
	method     string
	path       string
	headers    map[string]string
	body       string
	wantStatus int
	wantBody   string // substring or compact JSON
}

สำหรับอินพุตต่าง ๆ ใช้สตริง body เล็ก ๆ ที่แสดงความแตกต่างได้ชัด: payload ถูกต้อง หนึ่งฟิลด์หาย หนึ่งฟิลด์ชนิดผิด และสตริงว่าง หลีกเลี่ยงการสร้าง JSON ที่จัดรูปแบบมากในตาราง — มันจะดูวุ่นวายเร็ว

เมื่อคุณเห็นการตั้งค่าซ้ำ ๆ (การสร้าง token, headers ทั่วไป, body เริ่มต้น) ย้ายไปยัง helpers เช่น newRequest(tc) หรือ baseHeaders()

ถ้าตารางเดียวเริ่มผสมแนวคิดหลายอย่าง ให้แยกมัน ตารางหนึ่งสำหรับเส้นทางที่สำเร็จ และอีกตารางสำหรับข้อผิดพลาดมักอ่านและดีบักง่ายกว่า

การตรวจสอบ auth: เคสที่มักถูกข้าม

สร้างใหม่โดยไม่มี technical debt
อัปเดตความต้องการแล้วสร้างซอร์สโค้ดใหม่สะอาดแทนการแพตช์ handlers เก่า
สร้างโค้ด

การทดสอบ auth มักดูโอเคในเส้นทางที่สำเร็จ แต่ล้มเหลวใน production เพราะมีเคส "เล็ก ๆ" ที่ไม่ได้ถูกทดสอบ ถือ auth เป็นสัญญา: สิ่งที่ไคลเอนต์ส่ง สิ่งที่เซิร์ฟเวอร์คืน และสิ่งที่ต้องไม่ถูกเปิดเผย

เริ่มจากการมี/ไม่มี token และความถูกต้อง endpoint ที่ป้องกันควรแสดงพฤติกรรมต่างกันเมื่อ header ขาด กับเมื่อมีแต่ผิด หากคุณใช้ token ที่อายุสั้น ให้ทดสอบการหมดอายุด้วย แม้จะจำลองโดยการ inject validator ที่คืนค่า "expired"

ช่องว่างส่วนใหญ่ครอบคลุมโดยเคสดังนี้:

  • ไม่มี header Authorization -> 401 พร้อม response ข้อผิดพลาดที่สม่ำเสมอ
  • header ผิดรูปแบบ (prefix ผิด) -> 401
  • token ไม่ถูกต้อง (signature ผิด) -> 401
  • token หมดอายุ -> 401 (หรือรหัสที่คุณเลือก) พร้อมข้อความที่คาดเดาได้
  • token ถูกต้องแต่ role/permissions ผิด -> 403

การแยกระหว่าง 401 กับ 403 สำคัญ ใช้ 401 เมื่อผู้เรียกยังไม่ได้รับการยืนยันตัวตน ใช้ 403 เมื่อยืนยันตัวตนแล้วแต่ไม่มีสิทธิ์ ถ้าคุณเบลอทั้งสอง ไคลเอนต์อาจ retry โดยไม่จำเป็นหรือแสดง UI ผิด

การตรวจสิทธิ์ตามเจ้าของทรัพยากรก็สำคัญ (เช่น GET /orders/{id}) ทดสอบ ownership: ผู้ใช้ A ไม่ควรเห็นคำสั่งซื้อของผู้ใช้ B แม้มี token ถูกต้อง ควรเป็น 403 (หรือ 404 หากคุณซ่อนการมีอยู่ตั้งใจไว้) และ body ไม่ควรรั่วข้อมูล อย่าให้บอกว่า "order เป็นของ user 42"

กฎอินพุต: ตรวจสอบ ปฏิเสธ และอธิบายให้ชัด

ข้อบกพร่องก่อนปล่อยหลายอย่างมาจากอินพุต: ฟิลด์หาย ชนิดผิด รูปแบบไม่คาดคิด หรือ payload ใหญ่เกินไป

ตั้งชื่อทุกอินพุตที่ handler ยอมรับ: ฟิลด์ body JSON, query params และ path params สำหรับแต่ละตัว ตัดสินใจว่าเกิดอะไรขึ้นเมื่อมันหาย เปล่า ผิดรูปแบบ หรืออยู่นอกช่วง แล้วเขียนเคสที่พิสูจน์ว่า handler ปฏิเสธอินพุตที่ไม่ดีตั้งแต่ต้นและคืนข้อผิดพลาดแบบเดียวกันทุกครั้ง

ชุดเล็ก ๆ ของเคส validation มักครอบคลุมความเสี่ยงได้มากที่สุด:

  • ฟิลด์ที่ต้องมี: หาย vs สตริงว่าง vs null (ถ้าคุณอนุญาต)
  • ชนิดและรูปแบบ: number vs string, รูปแบบอีเมล/วันที่/UUID, การแปลง boolean
  • ข้อจำกัดขนาด: ความยาวสูงสุด, จำนวนสูงสุด, payload ใหญ่เกิน
  • ฟิลด์ไม่รู้จัก: ถูกละเลย vs ถูกปฏิเสธ (ถ้าคุณบังคับการ decode แบบเข้มงวด)
  • query และ path params: หาย, แปลงไม่ได้, และพฤติกรรมเริ่มต้น

ตัวอย่าง: handler POST /users รับ { "email": "...", "age": 0 } ทดสอบ email หาย, email เป็น 123, email เป็น "not-an-email", age เป็น -1, และ age เป็น "20" หากคุณต้องการ strict JSON ก็ทดสอบ { "email":"[email protected]", "extra":"x" } และยืนยันว่าล้มเหลว

ทำให้การล้มเหลวของ validation คาดเดาได้ เลือก status code สำหรับข้อผิดพลาด validation (ทีมบางทีมใช้ 400 บางทีมใช้ 422) และรักษารูปร่าง error body ให้สม่ำเสมอ การทดสอบควรยืนยันทั้งสถานะและข้อความ (หรือฟิลด์ details) ที่ชี้ไปยังอินพุตที่ล้มเหลว

สถานะโค้ดและ error bodies: ทำให้คาดเดาได้

ปรับใช้ที่คุณใช้งาน
ผลักดันแอปไปยัง AppMaster Cloud, AWS, Azure หรือ Google Cloud เมื่อคุณพร้อม
ปรับใช้แอป

การทดสอบ handler จะง่ายขึ้นเมื่อข้อผิดพลาดของ API น่าเบื่อและสม่ำเสมอ คุณต้องการให้ข้อผิดพลาดทุกชนิดแมปไปยัง status code ที่ชัดเจนและคืน JSON รูปร่างเดียวกัน ไม่ว่าคนเขียน handler จะเป็นใคร

เริ่มด้วยการแมปจากประเภทข้อผิดพลาดสู่ HTTP status codes เล็ก ๆ:

  • 400 Bad Request: JSON ผิดรูป แบบ query พารามิเตอร์ที่จำเป็นหาย
  • 404 Not Found: ไอดีทรัพยากรไม่มีอยู่
  • 409 Conflict: ขัดแย้งเรื่อง unique constraint หรือสถานะ
  • 422 Unprocessable Entity: JSON ถูกต้องแต่ล้มเหลวตามกฎธุรกิจ
  • 500 Internal Server Error: ความล้มเหลวที่ไม่คาดคิด (db down, nil pointer, บริการภายนอกล่ม)

จากนั้นรักษา error body ให้คงที่ แม้ข้อความจะแก้ไขภายหลัง ไคลเอนต์ควรพึ่งพาฟิลด์ที่คาดเดาได้:

{ "code": "user_not_found", "message": "User was not found", "details": { "id": "123" } }

ในการทดสอบ ให้ยืนยันรูปทรงของ JSON ไม่ใช่แค่ status บ่อยครั้งความล้มเหลวคือการคืน HTML, plain text หรือ body ว่างเมื่อเกิดข้อผิดพลาด ซึ่งทำให้ไคลเอนต์พังและปิดบังบั๊ก

ทดสอบ header และการเข้ารหัสสำหรับ error responses ด้วย:

  • Content-Type เป็น application/json (และ charset สม่ำเสมอถ้าคุณตั้ง)
  • body เป็น JSON ที่ถูกต้องแม้ในตอนเกิดข้อผิดพลาด
  • มี code, message, และ details (details อาจว่างแต่ไม่ควรสุ่ม)
  • panic และข้อผิดพลาดไม่คาดคิดคืน 500 ที่ปลอดภัยโดยไม่รั่ว stack traces

ถ้าคุณเพิ่ม recover middleware ให้มีการทดสอบหนึ่งเคสที่บังคับ panic และยืนยันว่ายังคงได้ JSON error ที่สะอาด

กรณีขอบ: ความล้มเหลว เวลา และเส้นทางที่ไม่สำเร็จ

ทำให้การตอบข้อผิดพลาดเป็นมาตรฐาน
ตั้งรูปแบบข้อผิดพลาด JSON มาตรฐานทั่วบริการและรักษาให้คงที่เมื่อคุณปรับปรุง
สร้างโปรเจกต์

การทดสอบเส้นทางที่สำเร็จพิสูจน์ว่า handler ทำงานครั้งหนึ่ง แต่การทดสอบกรณีขอบพิสูจน์ว่ามันยังคงทำงานเมื่อโลกไม่เป็นใจ

ทำให้ dependency ล้มเหลวในวิธีที่กำหนดและทำซ้ำได้ หาก handler เรียกฐานข้อมูล แคช หรือ API ภายนอก คุณต้องเห็นผลเมื่อชั้นเหล่านั้นคืนข้อผิดพลาดที่คุณควบคุมไม่ได้

สิ่งเหล่านี้ควรจำลองอย่างน้อยครั้งหนึ่งต่อ endpoint:

  • downstream timeout (context deadline exceeded)
  • storage คืนค่า not found เมื่อไคลเอนต์คาดว่าจะมีข้อมูล
  • unique constraint บน create (อีเมลซ้ำ, slug ซ้ำ)
  • network/transport error (connection refused, broken pipe)
  • ข้อผิดพลาดภายในที่ไม่คาดคิด ("something went wrong")

ทำให้การทดสอบเสถียรด้วยการควบคุมทุกอย่างที่อาจเปลี่ยนระหว่างการรัน เทสต์ที่ flaky แย่กว่าการไม่มีเทสต์เพราะทำให้คนมองข้ามความล้มเหลว

ทำให้เวลาและความสุ่มทำนายได้

ถ้า handler ใช้ time.Now(), ID หรือค่าที่สุ่มได้ ให้ inject พวกมัน ส่งฟังก์ชันนาฬิกาและตัวสร้าง ID เข้าไปใน handler หรือ service ในการทดสอบ ให้คืนค่าคงที่เพื่อคุณจะยืนยันฟิลด์ JSON และ header ได้แน่นอน

ใช้ fakes เล็ก ๆ และยืนยันว่า "ไม่มีผลกระทบข้างเคียง"

ชอบใช้ fakes หรือ stubs ขนาดเล็กกว่าการใช้ mocks เต็มรูปแบบ fake สามารถบันทึกการเรียกและให้คุณตรวจสอบว่ามีสิ่งใดเกิดขึ้นหรือไม่หลังจากความล้มเหลว

ตัวอย่าง ใน handler "create user" ถ้าการแทรกในฐานข้อมูลล้มเหลวด้วย unique constraint ให้ยืนยัน status code ถูกต้อง body ข้อผิดพลาดคงที่ และไม่มีอีเมลต้อนรับถูกส่ง fake mailer ของคุณสามารถเปิดเผยเคาน์เตอร์ (sent=0) เพื่อพิสูจน์ว่าเส้นทางล้มเหลวไม่ได้ทริกเกอร์ผลข้างเคียง

ข้อผิดพลาดทั่วไปที่ทำให้การทดสอบ handler ไม่น่าเชื่อถือ

การทดสอบ handler มักล้มเหลวด้วยเหตุผลผิด ๆ คำขอที่คุณสร้างในเทสต์ไม่เหมือนกับคำขอจากไคลเอนต์จริง นำไปสู่การล้มเหลวที่ไม่เกี่ยวกับโค้ดจริงและความเชื่อมั่นเท็จ

ปัญหาหนึ่งที่พบบ่อยคือส่ง JSON โดยไม่มี headers ที่ handler คาดหวัง ถ้าโค้ดเช็ค Content-Type: application/json การลืมมันอาจทำให้ handler ข้ามการ decode JSON คืนสถานะต่างกันหรือไปสาขาที่ไม่เกิดขึ้นใน production เช่นเดียวกับ auth: header Authorization หายไม่เท่ากับ token ไม่ถูกต้อง ทั้งสองควรเป็นเคสต่างกัน

กับกับดักอีกอย่างคือยืนยัน JSON ทั้งหมดเป็นสตริงดิบ การเปลี่ยนเล็กน้อยเช่นลำดับฟิลด์ การเว้นวรรค หรือเพิ่มฟิลด์ใหม่จะทำให้เทสต์พังแม้ API ยังถูกต้อง ให้ decode body เป็น struct หรือ map[string]any แล้วยืนยันเฉพาะสิ่งที่สำคัญ: สถานะ, รหัสข้อผิดพลาด, ข้อความ และฟิลด์สำคัญบางอย่าง

เทสต์ยังไม่เสถียรเมื่อเคสแชร์ state ที่เปลี่ยนได้ การใช้ที่เก็บข้อมูลในหน่วยความจำตัวเดียว ตัวแปร global หรือ router แบบ singleton ข้ามกรณีจะรั่วข้อมูลระหว่างเคส แต่ละเคสควรเริ่มสะอาด หรือรีเซ็ต state ใน t.Cleanup

รูปแบบที่มักทำให้เทสต์เปราะบาง:

  • สร้าง request โดยไม่มี header และการเข้ารหัสเหมือนที่ไคลเอนต์จริงใช้
  • ยืนยัน JSON ทั้งหมดเป็นสตริงแทนการ decode แล้วเช็คฟิลด์
  • ใช้ฐานข้อมูล/แคช/ตัวแปร global ร่วมกันข้ามเคส
  • ยัดการตรวจ auth, validation, และกฎธุรกิจในเทสต์เดียวที่ยาวเกินไป

เก็บแต่ละเทสต์ให้มีเป้าหมาย ถ้าเคสใดล้มเหลว คุณควรรู้ทันทีว่าเป็นปัญหา auth, อินพุต, หรือการฟอร์แมตข้อผิดพลาด

เช็คลิสต์ก่อนปล่อยใช้งานที่นำกลับมาใช้ได้

เชื่อมต่อการรวมที่พบบ่อย
เพิ่มการชำระเงิน Stripe, การส่งข้อความ หรือการรวม OpenAI และรักษาพฤติกรรม API ให้คงที่
สำรวจการรวม

ก่อนปล่อย การทดสอบควรพิสูจน์สองอย่าง: endpoint ปฏิบัติตามสัญญา และล้มเหลวในทางที่ปลอดภัยและคาดเดาได้

รันสิ่งเหล่านี้เป็นเคสแบบ table-driven และให้แต่ละเคสยืนยันทั้ง response และผลข้างเคียง:

  • Auth: ไม่มี token, token ผิด, role ผิด, role ถูกต้อง (และยืนยันว่าเคส "role ผิด" ไม่รั่วรายละเอียด)
  • อินพุต: ฟิลด์จำเป็นหาย, ชนิดผิด, ขอบเขตขนาด (min/max), ฟิลด์ไม่รู้จักที่คุณต้องการปฏิเสธ
  • เอาต์พุต: status code, header ที่สำคัญ (เช่น Content-Type), ฟิลด์ JSON ที่ต้องมี, รูปร่างข้อผิดพลาดที่สม่ำเสมอ
  • พึ่งพา: บังคับให้เกิดความล้มเหลวของ downstream (DB, queue, payment, email), ยืนยันข้อความที่ปลอดภัย, ยืนยันว่ายังไม่มีการเขียนบางส่วน
  • Idempotency: ทำคำขอเดิมซ้ำ (หรือ retry หลัง timeout) และยืนยันว่าไม่สร้างซ้ำ

หลังจากนั้น เพิ่มการยืนยันความสมเหตุสมผลหนึ่งข้อที่มักจะถูกข้าม: ยืนยันว่า handler ไม่ไปแตะต้องสิ่งที่ไม่ควรแตะ ตัวอย่างเช่น ในเคส validation ล้มเหลว ให้ยืนยันว่ายังไม่มีบันทึกถูกสร้างและไม่มีอีเมลถูกส่ง

ถ้าคุณสร้าง API ด้วยเครื่องมืออย่าง AppMaster, แนวทางเช็คลิสต์นี้ยังใช้ได้ จุดประสงค์เหมือนเดิม: ยืนยันพฤติกรรมสาธารณะยังคงคงที่

ตัวอย่าง: endpoint หนึ่ง ตารางเล็ก ๆ และสิ่งที่มันจับได้

สมมติคุณมี endpoint ง่าย ๆ: POST /login รับ JSON { "email", "password" } คืน 200 พร้อม token เมื่อสำเร็จ, 400 สำหรับอินพุตไม่ถูกต้อง, 401 สำหรับข้อมูลการเข้าสู่ระบบผิด และ 500 หากบริการ auth ล่ม

ตารางกะทัดรัดแบบนี้ครอบคลุมสิ่งที่มักพังใน production

func TestLoginHandler(t *testing.T) {
	// Fake dependency so we can force 200/401/500 without hitting real systems.
	auth := &FakeAuth{ /* configure per test */ }
	h := NewLoginHandler(auth)

	tests := []struct {
		name       string
		body       string
		authHeader string
		setup      func()
		wantStatus int
		wantBody   string
	}{
		{"success", `{"email":"[email protected]","password":"secret"}`, "", func() { auth.Mode = "ok" }, 200, `"token"`},
		{"missing password", `{"email":"[email protected]"}`, "", func() { auth.Mode = "ok" }, 400, "password"},
		{"bad email format", `{"email":"not-an-email","password":"secret"}`, "", func() { auth.Mode = "ok" }, 400, "email"},
		{"invalid JSON", `{`, "", func() { auth.Mode = "ok" }, 400, "invalid JSON"},
		{"unauthorized", `{"email":"[email protected]","password":"wrong"}`, "", func() { auth.Mode = "unauthorized" }, 401, "unauthorized"},
		{"server error", `{"email":"[email protected]","password":"secret"}`, "", func() { auth.Mode = "error" }, 500, "internal"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.setup()
			req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(tt.body))
			req.Header.Set("Content-Type", "application/json")
			if tt.authHeader != "" {
				req.Header.Set("Authorization", tt.authHeader)
			}

			rr := httptest.NewRecorder()
			h.ServeHTTP(rr, req)

			if rr.Code != tt.wantStatus {
				t.Fatalf("status = %d, want %d, body=%s", rr.Code, tt.wantStatus, rr.Body.String())
			}
			if tt.wantBody != "" && !strings.Contains(rr.Body.String(), tt.wantBody) {
				t.Fatalf("body %q does not contain %q", rr.Body.String(), tt.wantBody)
			}
		})
	}
}

เดินตามเคสหนึ่งเคสตั้งแต่ต้นจนจบ: สำหรับ "missing password" ส่ง body ที่มีแค่ email ตั้ง Content-Type รันผ่าน ServeHTTP แล้วยืนยัน 400 และ error ที่ชี้ชัดไปยัง password เคสนั้นพิสูจน์ว่า decoder, validator, และรูปแบบการตอบข้อผิดพลาดทำงานร่วมกันได้

ถ้าคุณต้องการวิธีที่เร็วขึ้นในการมาตรฐานสัญญา โมดูล auth และการรวมต่าง ๆ ในขณะที่ยังส่งโค้ด Go จริง ๆ AppMaster (appmaster.io) สร้างมาเพื่อสิ่งนั้น แม้ในกรณีนั้น การทดสอบเหล่านี้ยังมีคุณค่าเพราะล็อกพฤติกรรมที่ไคลเอนต์ของคุณพึ่งพาไว้

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม