CI/CD & Test Automation ในทางปฏิบัติ
การวาง Test Stage ใน Pipeline
Pipeline ที่ดีควรรัน test ตามลำดับ เร็ว/ถูกไปก่อน ช้า/แพงไปทีหลัง และ fail เร็วที่สุดที่ เป็นไปได้ เพื่อประหยัดเวลาและ compute
1. Lint/Static analysis (วินาที)
2. Unit tests (วินาทีถึงนาที)
3. API/Integration tests (นาที)
4. E2E tests (critical path) (นาทีถึงสิบนาที) — มักรันแค่ก่อน merge เข้า main หรือก่อน deploy
5. Smoke test บน staging/production หลัง deploy
"เรียงเทสต์เหมือนคัดกรองด่านตรวจ: ด่านถูกและเร็วตรวจก่อน ด่านแพงและช้าตรวจทีหลัง" — ไม่มีประโยชน์ ที่จะรอ E2E 20 นาทีจบก่อนรู้ว่า unit test fail ตั้งแต่ต้น ให้ unit test fail เร็วจะได้ feedback เร็วและประหยัด compute
เทคนิคที่ทีม production ใช้ลด feedback loop ให้เร็วขึ้นอีกคือ test impact analysis / affected test selection — แทนที่จะรัน test suite ทั้งหมดทุกครั้งที่มี commit (ซึ่งช้าลงเรื่อยๆ ตามขนาด codebase) ระบบจะวิเคราะห์ว่า commit นี้แก้ไฟล์ไหน แล้ว รันเฉพาะเทสต์ที่เกี่ยวข้องกับไฟล์ที่ เปลี่ยนจริง (อิง dependency graph) ทำให้ PR เล็กๆ ได้ feedback ในไม่กี่วินาทีแทนที่จะรอทั้ง suite — เครื่องมืออย่าง Bazel หรือ Nx ทำแบบนี้ได้ในระดับ build system เลย ส่วนทีมที่ยังไม่มี tooling ระดับนั้นมักใช้วิธีง่ายกว่าคือรัน full suite เฉพาะตอน merge เข้า main/nightly build และรันแค่ subset ที่เกี่ยวข้องตอน PR แทน
Flaky Test ในบริบท CI/CD
Flaky test ที่ไม่ถูกจัดการดีจะทำลาย ความเชื่อมั่นต่อ CI ทั้งระบบ — เมื่อคนเริ่ม "เห็น fail สีแดงก็กด re-run เฉยๆ" โดยไม่สงสัยว่ามีบั๊กจริง จุดนี้คือจุดที่ CI เสียประโยชน์หลักของมันไปแล้ว
แนวทางจัดการที่เป็นระบบ:
- ตรวจจับ: tool/dashboard ที่ track test ไหน fail ซ้ำๆ แบบไม่สัมพันธ์กับ code change (flaky test detection)
- Quarantine: แยกเทสต์ที่ flaky ออกจาก required check ชั่วคราว พร้อม ticket ติดตามแก้
- Root-cause: หาสาเหตุจริง (เวลา, test order/shared state, race condition, network ที่ไม่ fake) — ดู Unit Testing → Interview Prep สำหรับ รายละเอียดสาเหตุ
- ห้าม: เพิ่ม retry อัตโนมัติเป็นวิธีแก้ถาวร (ซ่อนปัญหา ไม่ใช่แก้)
"Flaky test เหมือนสัญญาณเตือนไฟไหม้ที่ร้องเล่นๆ บ่อยเกินไป" — ถ้าร้องบ่อยจนคนเริ่มเมินเฉย วันที่ ไฟไหม้จริงคนก็จะเมินเฉยเหมือนกัน ต้องซ่อมสัญญาณให้แม่นยำ ไม่ใช่ปล่อยให้ร้องเล่นๆ ต่อไป
มี flaky test ประเภทหนึ่งที่วินิจฉัยยากเป็นพิเศษ: "order-dependent flakiness ที่โผล่เฉพาะตอน รันแบบ parallel ใน CI" แต่รันผ่านปกติทุกครั้งตอน dev รันบนเครื่องตัวเอง (รันทีละเทสต์/ไม่ parallel) สาเหตุมักมาจาก shared mutable state ที่ซ่อนอยู่ (เช่น ใช้ global variable, แก้ ไฟล์เดียวกันในระบบไฟล์ชั่วคราว, หรือใช้ database connection/schema เดียวกันข้ามเทสต์โดยไม่ได้ ตั้งใจ) เทสต์สองตัวที่ไม่เกี่ยวกันเลยในสายตาคนเขียนอาจ "ชนกัน" เมื่อรันพร้อมกันบน CI ที่มี parallel worker หลายตัว วิธี debug ที่ได้ผลคือพยายาม reproduce โดยรันเฉพาะคู่เทสต์ที่สงสัยว่า ชนกันแบบ parallel ซ้ำๆ บนเครื่อง local แทนที่จะพยายามไล่อ่าน CI log ที่ noise เยอะ
Test Data Management
เทสต์ที่ดีต้อง independent (ตามหลัก FIRST — ดู Unit Testing) ซึ่งรวมถึงการจัดการ test data ให้ไม่ชนกันข้ามเทสต์/ข้าม run
แนวทางที่ใช้บ่อย:
- Fresh data ต่อ test run: ใช้ factory/builder สร้างข้อมูลใหม่ทุกครั้ง (เช่น random email ต่อ test run) ไม่ hardcode ค่าคงที่ที่ชนกันได้เมื่อรัน parallel
- Isolated environment: ใช้ test container (database แยกต่อ test run) หรือ transaction rollback หลังจบแต่ละเทสต์ เพื่อไม่ทิ้งข้อมูลค้าง
- Seed data ที่ reproducible: ถ้าต้องมีข้อมูลตั้งต้น ให้ seed ผ่าน script ที่รันซ้ำได้ผล เดิมเสมอ ไม่ใช่พึ่งข้อมูลที่มีอยู่แล้วใน environment ซึ่งอาจถูกเทสต์อื่นแก้ไปแล้ว
"Test data ที่ดีต้อง 'เกิดใหม่ทุกครั้ง' ไม่ใช่ 'ยืมของเก่ามาใช้ซ้ำ'" — ของยืมมีสิทธิ์ถูกคนอื่น ใช้ไปแล้วหรือเปลี่ยนสภาพไปแล้วโดยไม่รู้ตัว ทำให้เทสต์ผลลัพธ์ไม่แน่นอน
กับดักที่พบบ่อยในทีมที่เพิ่งย้ายมา parallel test execution คือ "transaction rollback ไม่ครอบ side effect ที่อยู่นอก database" เช่น เทสต์ที่เขียนไฟล์ลง disk, ส่ง event เข้า message queue จริง, หรือเรียก external API แม้จะ rollback database transaction ได้สำเร็จ แต่ side effect เหล่านี้ยังหลุดออกไปค้างอยู่ (ไฟล์ยังอยู่บน disk, event ยังอยู่ใน queue) ทำให้เทสต์ที่รันถัดไป เจอสภาพแวดล้อมที่ "ไม่สะอาด" เหมือนที่คิดไว้ การจัดการที่ถูกต้องต้องแยกให้ชัดว่า dependency ตัว ไหนทำ rollback อัตโนมัติได้ (database) และตัวไหนต้อง mock/fake แทนการเรียกจริง ตั้งแต่แรก (message queue, external API, filesystem) ไม่ใช่พึ่ง transaction rollback เป็นทางออกเดียว
Order pipeline stages fast-and-cheap first so failures surface quickly and expensively-slow stages like E2E run last, ideally gated to merge/pre-deploy only; mature teams add test impact analysis to run only tests affected by a given change. Flaky tests must be detected, quarantined, and root-caused — never papered over with auto-retry, since that can mask real production race conditions; parallel-only flakiness usually traces to hidden shared mutable state invisible when running tests serially. Test data should be freshly generated per run rather than reused fixed values, and database transaction rollback doesn't clean up side effects outside the database (files, queues, external API calls) — those need mocking rather than relying on rollback alone.