🛑 1. The Problem First: "Test ผ่านทุุกข้อ... แต่พอรันจริงกลับพัง"
ลองนึกถึงวันที่คุณเขียน Unit Test อย่างบ้าคลั่งจนได้ Code Coverage 100%:
// ❌ Naive Approach: บ้าเขียน Unit Test ให้กับทุุกฟังก์ชันเล็กๆ
test("button should change state when clicked", () => {
const { result } = renderHook(() => useButton());
act(() => result.current.toggle());
expect(result.current.active).toBe(true);
});
// 🌋 พัง! พอขึ้นระบบจริง ปรากฏว่าปุ่มนั้น 'จม' หายไปอยู่ใต้รูปภาพเพราะ CSS พัง
// หรือ API ส่งค่ามาไม่ตรงที่ปุ่มคาดหวัง Unit Test ของคุณที่แยกเทสทีละชิ้น
// ไม่ได้รับประกันเลยว่า 'ระบบโดยรวม' จะทำงานได้จริง
ปัญหา: การมี Unit Test เยอะไม่ได้หมายความว่าระบบของคุณจะเสถียร ยิ่งคุณเขียน Test ผูกติดกับ Logic ข้างในมากเท่าไหร่ (Implementation Details) เวลาคุณจะแก้โค้ดเพียงนิดเดียว Test จะพังระเนระนาดจนคุณไม่อยากเขียน Test อีกเลย นี่คือ "หนี้ทางเทคนิค" ที่เกิดจากความหวังดีครับ
💡 2. Real-Life Analogy: การตรวจเช็ครถยนต์ก่อนออกจากโรงงาน
- Static Testing (TypeScript): เหมือน "แม่พิมพ์ชิ้นส่วน". ถ้าชิ้นส่วนไม่ได้รูปตามแบบ (Type ผิด) มันจะประกอบเข้ากับชิ้นอื่นไม่ได้ตั้งแต่แรก
- Unit Testing: เหมือน "การเทสลูกสูบเครื่องยนต์". เทสว่าลูกสูบขยับขึ้นลงได้ถูกต้องไหม (Logic ของฟังก์ชัน) แต่ไม่ได้บอกว่าเครื่องยนต์จะติดไฟเดินหน้าได้หรือเปล่า
- Integration Testing: เหมือน "การยกเครื่องยนต์ใส่ตัวถังแล้วบิดกุญแจ". เทสว่าทุุกส่วน (Component + State + API) ทำงานร่วมกันได้จริงไหม นี่คือจุดที่สำคัญที่สุด!
- End-to-End (E2E): เหมือน "การเอารถออกไปขับบนถนนจริง". ดูว่าทุุกอย่างตั้งแต่พวงมาลัยยันเบรคทำงานสอดประสานกันท่ามกลางการจราจรจริงๆ
🚀 3. Execution Journey: ขั้นตอนการสร้างอาวุธลับ (The Testing Trophy)
เลิกแบกภาระ Unit Test 100% แล้วย้ายมาเน้น Integration Test ที่ใช้การกระทำของ User เป็นตัวตั้ง
🛠 Step-by-step:
- The Static Guard: เปิดโหมด Strict ใน TypeScript เพื่อจับบั๊กโง่ๆ (Typos) ก่อนใครเพื่อน
- The Integration Hero: ใช้
React Testing Libraryเพื่อเทส "สิ่งที่ User เห็น" (เช่น การกดปุ่มแล้วคาดหวังข้อความขึ้น) แทนการเช็ค State ภายใน - The Logic Guard: เลือกทำ Unit Test เฉพาะ Pure Logic ที่ซับซ้อนจริงๆ (เช่น สูตรคำนวณ)
- The Critical Path: เขียน E2E Test (Playwright) เฉพาะ Flow ที่ "ห้ามพังเด็ดขาด" เช่น Login และ Checkout
// ✅ Best Practice: Integration Test ที่ยั่งยืน (RTL)
test("User can search and see results", async () => {
render(<SearchPage />);
const input = screen.getByPlaceholderText(/search/i);
fireEvent.change(input, { target: { value: "iPhone" } });
fireEvent.click(screen.getByRole("button", { name: /search/i }));
// 🛠 คาดหวัง "ผลลัพธ์ที่ User จะเห็น" ไม่ใช่มองไปที่ State ภายใน
expect(await screen.findByText(/results for iphone/i)).toBeInTheDocument();
});
🪤 4. The Junior Trap: โรค "Flaky Tests" (Test ที่เดี๋ยวเขียวเดี๋ยวแดง)
จูเนียร์มักจะเขียน E2E Test ให้กับทุุกหน้า และพยายามรันมันทุุกนาที:
// ❌ Junior Trap: เขียน E2E เยอะจนเกินจำเป็น
test('Header should be visible', () => { ... });
test('Footer should have copyright', () => { ... });
// 🌋 พัง! E2E Test รันช้าและพังง่าย (เน็ตสะดุดนิดเดียวก็แดง)
// การมี Test ที่ 'แดงมั่วๆ' บ่อยครั้ง จะทำให้ทีมหมดความเชื่อถือในระบบ Test
// จนสุดท้ายไม่มีใครสนใจมองมันอีกเลย
ระวัง: ทุุกวินาทีที่รัน Test มีต้นทุนของเวลาและทรัพยากร ✅ การแก้ไข: จงทำ E2E ให้น้อยแต่ 'แม่น' (Only Happy Paths/Critical Paths) และปล่อยให้เคสยิบย่อยเป็นหน้าที่ของ Integration Test ครับ
⚖️ 5. The Why Matrix: Testing Pyramid vs Testing Trophy
| หัวข้อ | Testing Pyramid (Old School) | Testing Trophy (Modern) |
|---|---|---|
| น้ำหนักมากที่สุด | Unit Test (เทสทีละชิ้น) | Integration Test (เทสการทำงานร่วมกัน) |
| ความมั่นใจ | ปานกลาง | 🚀 สูงมาก (ใกล้เคียง User จริง) |
| ความยืดหยุ่น (Refactor) | ต่ำ (แก้โค้ดนิดเดียว Test พัง) | 🚀 สูง (เน้น User Behavior) |
| ความเร็วในการรัน | ⚡⚡⚡ เร็วที่สุด | ⚡⚡ รวดเร็วพอใช้ได้ |
🎓 6. Senior Mindset Summary
การเป็น Senior คือการมองว่า "เราไม่ได้เขียน Test เพื่อให้ดูเท่ แต่เพื่อเป็นใบประกันความมั่นใจว่า เราสามารถนอนหลับสบายได้ในวันที่ส่งของใหม่ขึ้นโปรดักชัน". จงเลือกทุ่มเทเวลาให้กับการเทสที่ให้ความมั่นใจสูงสุดด้วยความพยายามที่เหมาะสมที่สุดครับ!