Skip to main content

Unit Testing Interview Prep — คำถามที่พบบ่อย

1. ทำไม Test ก่อน Code (TDD) ถึงดีกว่า Test หลัง Code?​

TDD (เขียนเทสต์ก่อน เห็น fail แดง → เขียนโค้ดให้ผ่าน เห็นเขียว → refactor) บังคับให้คิดถึง interface/API ของโค้ดก่อนรายละเอียด implementation และบังคับให้ code ทุกบรรทัดมีเทสต์ รองรับจริง (ไม่ใช่เขียนโค้ดเสร็จแล้ว "ลืม" เขียนเทสต์บาง edge case) ข้อเสียคือช้ากว่าตอนเริ่ม และไม่เหมาะกับงานที่ยัง exploratory มาก (ยังไม่รู้ว่า design สุดท้ายจะเป็นยังไง)

🧠 จำง่าย + 🎯 Beyond

จำ cycle "แดง-เขียว-รีแฟคเตอร์" (Red-Green-Refactor) — Beyond: ประโยชน์ของ TDD ที่คนมัก มองข้ามไม่ใช่แค่ "ได้ coverage สูง" แต่คือ มันบังคับให้ design ผ่าน consumer's perspective ก่อนเสมอ (เพราะต้องเขียนโค้ดเรียกใช้ฟังก์ชันที่ยังไม่มีอยู่จริงตอนเขียนเทสต์) ทำให้ API ที่ ได้มักใช้งานง่ายกว่า API ที่ออกแบบจาก "มุมมองคน implement" ล้วนๆ — แต่ข้อเสียที่ต้องพูดถึงคู่กัน คือ TDD ที่เข้มงวดเกินไปกับโค้ดที่ design ยังไม่นิ่ง (เช่น prototype/spike) อาจทำให้เสียเวลา เขียนเทสต์ซ้ำๆ ทุกครั้งที่ design เปลี่ยนทิศทาง — หลายทีม pragmatic จึงใช้ TDD เฉพาะกับโค้ด business logic ที่ requirement ชัดเจนแล้ว ไม่ใช้กับ exploratory code ทั้งหมด

2. อธิบายความแตกต่างระหว่าง Unit Test กับ Integration Test ด้วยตัวอย่างจริง​

Unit test ทดสอบ OrderService.CalculateTotal() โดย mock PricingRepository ให้ตอบราคา คงที่ (ไม่แตะ database จริง) — Integration test ทดสอบ OrderService + PricingRepository

  • database จริง (หรือ test container) ทำงานร่วมกันจริง เพื่อจับบั๊กที่เกิดจาก SQL query ผิด, mapping ข้อมูลผิด, transaction handling ผิด ที่ unit test (เพราะ mock ไปแล้ว) จับไม่ได้เลย
🧠 จำง่าย + 🎯 Beyond

จำว่า "Unit test เชื่อใจ mock, Integration test เชื่อใจของจริง" — Beyond: คำถาม follow-up ที่พบบ่อยคือ "แล้ว integration test ควรใช้ database จริงหรือ test container (เช่น Testcontainers ที่ spin up Docker container ชั่วคราว)" คำตอบที่ดีควรพูดถึง trade-off: database จริงที่ share กันข้ามทีม/CI run เสี่ยง test pollution (เทสต์หนึ่งทิ้งข้อมูลค้างไว้ กระทบเทสต์อื่น) และ flaky จาก concurrent CI run ชนกัน ส่วน test container ให้ database สด ใหม่ทุกรอบ (isolated, repeatable ตามหลัก FIRST) แต่ overhead ของการ spin up container ทุก test run ทำให้ suite ช้าลงกว่าเดิมพอสมควร — ทีม production ส่วนใหญ่ยอมรับ overhead นี้เพื่อ แลกกับความน่าเชื่อถือที่สูงกว่ามาก

3. Flaky Test คืออะไร และจะแก้ยังไง?​

Flaky test คือเทสต์ที่ บางครั้งผ่าน บางครั้ง fail โดยที่โค้ดไม่เปลี่ยนเลย — ทำลายความเชื่อ มั่นของทีมต่อ test suite ทั้งหมด (เจอ fail แล้วคนเริ่ม "rerun เอา" แทนที่จะสงสัยว่ามีบั๊กจริง ซึ่งอันตรายมาก เพราะวันหนึ่งอาจมี fail จริงที่โดนมองข้ามไปเพราะเข้าใจผิดว่าเป็น flaky เหมือน ที่ผ่านมา)

🧠 จำง่าย + 🎯 Beyond

จำสาเหตุหลัก 4 อย่างของ flaky test: "เวลา, ลำดับ, concurrency, network" (time-dependent logic, test order dependency/shared state, race condition ในโค้ดที่ทดสอบเอง, external network call ที่ไม่ fake) — Beyond: วิธีแก้ flaky test ที่ "ผิดแต่พบบ่อยที่สุด" คือการเพิ่ม retry ให้เทสต์ fail แล้วลองใหม่อัตโนมัติ (เช่น ใส่ retry: 3 ใน CI config) ซึ่งซ่อนปัญหา ไว้ใต้พรมแทนที่จะแก้ — ถ้าเทสต์ flaky เพราะมี race condition จริงในโค้ด production (ไม่ใช่ แค่ปัญหาของตัวเทสต์เอง) การ retry จนผ่านจะทำให้ทีมมองไม่เห็นบั๊กที่ผู้ใช้จริงอาจเจอ race condition เดียวกันนี้ในระบบจริงด้วย วิธีแก้ที่ถูกต้องคือ quarantine เทสต์ที่ flaky (แยก ออกจาก required check ชั่วคราว พร้อม tag/ticket ติดตาม) แล้วสืบสาเหตุจริงให้เจอ ไม่ใช่ปล่อย retry ทับไปเรื่อยๆ

4. ถ้าต้อง Review โค้ดที่มี Test Coverage 95% แต่ยัง Production Bug บ่อย จะคิดยังไง?​

ตัวเลข coverage สูงไม่ได้รับประกันคุณภาพเทสต์ (ตามที่อธิบายใน Coverage) — ต้องเจาะดูว่า: assertion ในเทสต์เหล่านั้นแข็งแรงจริงไหม (ไม่ใช่แค่ "ไม่ throw"), edge case สำคัญถูกครอบไหม (ไม่ใช่แค่ happy path), และ bug ที่เกิดขึ้นจริงอยู่ "ประเภทไหน" (unit-level logic bug vs integration-level bug ที่ unit test เห็นไม่ได้เพราะ mock ไปหมด)

🧠 จำง่าย + 🎯 Beyond

จำว่า "Coverage สูงบอกแค่ว่าเทสต์ 'คลุมพื้นที่' ไม่ได้บอกว่า 'คลุมคุณภาพ'" — Beyond: คำตอบ ที่แสดงความเข้าใจลึกคือเสนอให้วิเคราะห์ bug ที่เกิดจริงย้อนหลัง (bug postmortem) แล้วจัด กลุ่มว่าเป็นบั๊กประเภทไหน ถ้าส่วนใหญ่เป็น integration-level bug (เช่น service คุยกันผิด contract) ก็ชี้ว่าปัญหาไม่ได้อยู่ที่ "unit test coverage ไม่พอ" แต่อยู่ที่ "test pyramid ไม่สมดุล" (integration test น้อยเกินไป) ซึ่งเป็นคำตอบที่เชื่อมโยงกลับไปที่หัวข้อ Test Pyramid ได้อย่างเป็นระบบ แทนที่จะตอบแค่ผิวเผินว่า "ต้องเขียน เทสต์เพิ่ม" โดยไม่วิเคราะห์ว่าควรเพิ่มเทสต์ "ประเภทไหน" กันแน่

English Summary

TDD's Red-Green-Refactor cycle forces API design from the consumer's perspective before implementation details, though strict TDD can waste effort on still-exploratory code where design keeps shifting. Unit tests trust mocks; integration tests catch bugs mocks hide (bad SQL, mapping errors, transaction issues) — Testcontainers trade suite speed for isolation/ repeatability over shared databases. Flaky tests typically stem from time-dependence, order/ shared-state dependency, race conditions, or un-faked network calls; auto-retrying flaky tests in CI is a common but harmful fix, since it can mask real production race conditions — quarantining and root-causing is the correct approach. High coverage with frequent production bugs usually means weak assertions, missed edge cases, or an imbalanced test pyramid (not enough integration coverage for cross-service bugs) rather than simply "not enough tests."