🛑 1. The Problem First: "Test ที่เขียวแต่พัง และ Test ที่พังแต่แอปปกติ"
ลองนึกถึงวันที่คุณเขียน Test เพื่อตรวจสอบการกดปุ่ม:
// ❌ Naive Approach: Test ตามโครงสร้างโค้ด (Brittle Test)
const { container } = render(<MyButton />);
const btn = container.querySelector('.btn-blue-primary');
expect(btn.innerHTML).toBe('Click me');
// 🌋 พัง! วันรุ่งขึ้นคุณเปลี่ยน CSS จากสีฟ้าเป็นสีแดง (.btn-red)
# แม้ปุ่มจะยังทำงานได้ปกติ แต่ Test กลับ 'พัง' ทันทีเพราะคุณไปยึดติดกับชื่อ Class
# นี่คือสาเหตุที่หลายทีมมองว่าการเขียน Test คือภาระที่เสียเวลาครับ
ปัญหา: การเขียน Test ที่ยึดติดกับ "วิธีการ" (Implementation Details) เช่น ชื่อตัวแปร, ลำดับของ Div, หรือชื่อ Class จะทำให้ทุุกครั้งที่คุณ Refactor โค้ดให้สะอาดขึ้น คุณต้องเสียเวลามานั่งแก้ Test ตามไปด้วย จนสุดท้ายทุุกคนก็เลิกเขียน Test ไปในที่สุดครับ
💡 2. Real-Life Analogy: การตรวจรับรถยนต์จากโรงงาน
- White-box Test (แบบเก่า): เหมือนคุณมุดลงไปใต้ท้องรถเพื่อเช็คว่า "น็อตตัวที่ 5 เป็นสีเงินไหม?" (ถ้าโรงงานเปลี่ยนเป็นน็อตสีดำที่แข็งแรงกว่าเดิม คุณจะบอกว่ารถคันนี้พังทันที ทั้งที่รถมันดีขึ้น)
- Behavioral Test (RTL Philosophy): เหมือนคุณนั่งบนเบาะคนขับแล้วลอง "เหยียบเบรก" ดูว่ารถหยุดไหม? หรือ "หมุนกุญแจ" แล้วเครื่องติดไหม? (ไม่ว่าข้างในจะใช้สายไฟสีอะไร ตราบใดที่เหยียบเบรกแล้วรถหยุด รถคันนี้คือรถที่ผ่านการทดสอบ)
- Accessibility Roles: เหมือนการมองหาปุ่มโดยใช้สัญชาตญาณคนทั่วไป "มองหาปุ่มที่เขียนว่า Login" แทนที่จะบอกว่า "มองหาวัตถุที่อยู่พิกัด X, Y"
🚀 3. Execution Journey: มหากาพย์การสร้าง Test ที่คงกระพัน
Senior จะเขียน Test ที่จะยัง 'เขียว' (ผ่าน) ตราบใดที่หน้าตาและพฤติกรรมของ User ยังเหมือนเดิม
🛠 Step-by-step:
- The Role Search: ใช้
screen.getByRoleเสมอ (เช่น button, heading, textbox) เพื่ออ้างอิงถึงความหมายของ UI ไม่ใช่แค่หน้าตา - The User Event: ใช้
@testing-library/user-eventแทนfireEventเพื่อจำลองทุุกขั้นตอนที่คนทำจริงๆ (เช่น มีการ Focus ก่อนพิมพ์) - The Async Wait: ใช้
findBy...(ที่มี await) เมื่อต้องรอข้อมูลจาก API เพื่อจัดการกับสถานะ loading อย่างถูกต้อง - The Snapshot Guard: ใช้ Snapshot เฉพาะส่วนที่ต้องการคุมความนิ่งของ UI จริงๆ ไม่ใช่ครอบทั้้งหน้าจนหาจุดผิดไม่เจอ
// ✅ Best Practice: การเขียน Test แบบ Senior (Behavioral)
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("ผู้ใช้ต้องสามารถ Login ได้สำเร็จเมื่อกรอกข้อมูลครบ", async () => {
render(<LoginForm />);
// 🛠 ค้นหาด้วยความหมาย (Accessibility)
await userEvent.type(screen.getByLabelText(/อีเมล/i), "admin@test.com");
await userEvent.click(screen.getByRole("button", { name: /เข้าสู่ระบบ/i }));
// 🛠 ตรวจสอบผลลัพธ์ที่ User สัมผัสได้จริง
expect(await screen.findByText(/ยินดีต้อนรับ/i)).toBeInTheDocument();
});
🪤 4. The Junior Trap: โรค "TestId Overuse"
จูเนียร์มักจะขี้เกียจหาชื่อ Role เลยประทับตรา data-testid ลงไปทุุกจุด:
// ❌ Junior Trap: สาด TestId ไปทั่ว Component
<div data-testid="user-container">
<button data-testid="submit-btn" />
</div>
// 🌋 พัง! การใช้ TestId มากเกินไปทำให้โค้ดเราสกปรกและ 'ทาบไปกับพฤติกรรมจริง' ไม่ได้
# เพราะ User ตัวจริงไม่มีทางเห็น data-testid พวกนี้ เขาเห็นแค่ปุ่มและข้อความ
ระวัง: data-testid คือทางออกสุดท้ายเมื่อคุณไม่สามารถหาของด้วยวิธีอื่นได้จริงๆ
✅ การแก้ไข: จงพยายามปรับปรุง HTML ให้มีความหมาย (Semantic) จนกระทั่งคุณสามารถหาของด้วย getByRole หรือ getByLabelText ได้ครับ
⚖️ 5. The Why Matrix: Implementation Test vs Behavioral Test
| หัวข้อ | Implementation Test (พังง่าย) | Behavioral Test (Senior) |
|---|---|---|
| ความทนทาน | 🐢 ต่ำ (แก้โค้ดนิดเดียว Test พัง) | 🚀 สูงสุด (ทนต่อการ Refactor) |
| ความเชื่อมั่น | ปานกลาง (อาจจะผ่านแต่แอปพังจริง) | 🚀 สูงมาก (ถ้าผ่านคือ User ใช้งานได้) |
| ความเร็วในการรัน | ⚡ เร็วมาก | ปานกลาง (มีการจำลอง Event) |
| เอกสาร (Documentation) | อ่านไม่รู้เรื่องว่าแอปทำอะไร | 😍 ดีมาก (อ่าน Test แล้วรู้เลยว่า App ทำงานยังไง) |
🎓 6. Senior Mindset Summary
การเป็น Senior คือการมองว่า "เราเขียน Test เพื่อให้เรากล้าเปลี่ยนโค้ด ไม่ใช่เปลี่ยนเพื่อให้ Test มันผ่าน". Test ที่ดีคือเพื่อนคู่ใจที่จะบอกคุณว่า "ลุยเลย! คุณจัดระเบียบโค้ดได้เต็มที่ พฤติกรรมทุุกอย่างยังปกติดี" นั่นคือหัวใจของความมั่นคงในระยะยาวครับ!