ทำความรู้จักกับ Flaky Tests: ปีศาจร้ายในระบบทดสอบอัตโนมัติ
ในฐานะนักพัฒนาซอฟต์แวร์มือใหม่ คุณอาจเคยเจอปัญหาที่ว่า รันโค้ดชุดเดิมเป๊ะๆ แต่ผลลัพธ์กลับไม่เหมือนเดิม บางครั้งรันผ่าน บางครั้งรันไม่ผ่าน ทั้งที่ไม่ได้แก้โค้ดอะไรเลย สถานการณ์นี้เรียกว่า Flaky Tests หรือการทดสอบที่ไม่มีความเสถียร ซึ่งเป็นปัญหาใหญ่ที่บั่นทอนความมั่นใจในระบบ CI/CD (Continuous Integration/Continuous Deployment) หรือกระบวนการส่งมอบซอฟต์แวร์อัตโนมัติของเรา
ลองนึกภาพว่าคุณกำลังทำขนมปังตามสูตรเดิมทุกขั้นตอน แต่วันนี้เตาอบทำงานปกติ พรุ่งนี้เตาอบดันร้อนไม่เท่ากันจนขนมไหม้ ถ้าเป็นแบบนี้คุณจะเลิกเชื่อใจเตาอบเครื่องนั้นทันที เช่นเดียวกันครับ ถ้าโปรแกรมเมอร์เริ่มมองข้ามการแจ้งเตือนจากระบบทดสอบเพราะคิดว่า "เดี๋ยวรันใหม่ก็ผ่านเอง" นั่นคือสัญญาณเตือนว่าระบบของคุณกำลังสูญเสียความน่าเชื่อถืออย่างรุนแรง
การปล่อยให้ Flaky Tests อยู่ในโปรเจกต์จะทำให้ทีมเสียเวลาเปล่า (Engineering Cycles) ในการมานั่งลุ้นว่าผลทดสอบที่พังเป็นเพราะโค้ดเราห่วยจริง หรือเป็นเพราะระบบทดสอบมันรวนเอง ดังนั้นการจัดการกับมันจึงไม่ใช่แค่เรื่องของเทคนิค แต่เป็นเรื่องของ วินัยและความเป็นมืออาชีพ ที่คุณต้องเริ่มฝึกฝนตั้งแต่วันนี้เพื่อให้โปรเจกต์ของคุณดูน่าเชื่อถือและทำงานได้จริง
หัวใจสำคัญของการเขียน Test ให้เสถียร: เริ่มจากความโดดเดี่ยว
เทคนิคแรกที่ต้องจำให้ขึ้นใจคือ Test Isolation หรือการทำให้การทดสอบแต่ละตัวแยกจากกันโดยเด็ดขาด Stateless (การที่โปรแกรมไม่จดจำสถานะเดิม) คือเป้าหมายสูงสุด เพราะถ้าการทดสอบตัวที่ 1 ไปสร้างไฟล์ทิ้งไว้ แล้วการทดสอบตัวที่ 2 มาอ่านไฟล์นั้นซ้ำ ผลลัพธ์อาจจะเพี้ยนได้ทันทีหากลำดับการรันเปลี่ยนไป
คุณต้องมั่นใจว่าก่อนเริ่มการทดสอบแต่ละครั้ง ระบบจะถูกล้างให้สะอาดเหมือนเพิ่งเปิดเครื่องใหม่ คุณอาจต้องเคลียร์ฐานข้อมูล (Database), ลบไฟล์ที่ดาวน์โหลดมา หรือล้างค่า localStorage (พื้นที่เก็บข้อมูลในเบราว์เซอร์) ออกให้หมด เพื่อให้มั่นใจว่าทุกครั้งที่รัน Automated Tests มันจะเริ่มจากจุดสตาร์ทเดียวกันเสมอ
ตัวอย่างการล้างข้อมูลใน Playwright (เครื่องมือทดสอบเว็บยอดนิยม) เพื่อให้แน่ใจว่าการทดสอบแต่ละครั้งเป็นอิสระต่อกัน:
// ใช้ beforeEach เพื่อล้างค่าก่อนเริ่มทดสอบทุกครั้ง
test.beforeEach(async ({ page }) => {
// ล้างข้อมูลใน localStorage เพื่อป้องกันผลกระทบจากเทสต์อื่น
await page.evaluate(() => window.localStorage.clear());
// ไปยังหน้าเริ่มต้นใหม่เสมอ
await page.goto('https://myapp.com/login');
});
จากโค้ดด้านบน เราใช้ beforeEach เพื่อให้มั่นใจว่าทุกครั้งที่เริ่มเทสต์ใหม่ ระบบจะเคลียร์ข้อมูลเก่าทิ้งเสมอ ทำให้เราไม่ต้องกังวลว่าข้อมูลขยะจากเทสต์ก่อนหน้าจะมาทำให้เทสต์ปัจจุบันพังโดยไม่ตั้งใจ
เลือกใช้ Selector ให้ฉลาด: อย่าพึ่งพาความเปราะบาง
บ่อยครั้งที่มือใหม่เลือกใช้ CSS Selector ที่ผูกติดกับโครงสร้างหน้าเว็บ เช่น div > ul > li:nth-child(2) > a ซึ่งถ้าวันหนึ่งดีไซน์เนอร์เปลี่ยนโครงสร้างหน้าเว็บนิดเดียว เทสต์ของคุณก็จะพังทันที นี่คือสาเหตุอันดับต้นๆ ของความไม่เสถียรที่เราเรียกว่า Fragile Selectors
วิธีแก้ที่ดีที่สุดคือการสร้าง Dedicated Test Attributes หรือการสร้างแอตทริบิวต์พิเศษไว้ให้เทสต์โดยเฉพาะ เช่นการเติม data-test-id เข้าไปใน HTML ของเรา วิธีนี้จะแยกส่วนของการแสดงผล (UI) ออกจากการทดสอบ (Testing) ทำให้เมื่อคุณแก้ดีไซน์ เทสต์ก็ยังคงทำงานได้ถูกต้องเหมือนเดิม
เปรียบเทียบระหว่างวิธีที่พังง่าย กับวิธีที่แนะนำให้ใช้:
- แบบที่พังง่าย: ใช้
button.btn-primary(ถ้าเปลี่ยนสีปุ่มหรือเปลี่ยนคลาส เทสต์จะพัง) - แบบที่แนะนำ: ใช้
[data-test-id='login-submit-button'](ไม่ว่าปุ่มจะเปลี่ยนสีหรือย้ายตำแหน่งไปแค่ไหน เทสต์ก็ยังหาปุ่มเจอ)
การตั้งชื่อแบบนี้ช่วยให้คนในทีมรู้ทันทีว่า "นี่คือส่วนที่เกี่ยวข้องกับการทดสอบนะ ห้ามลบออกหรือเปลี่ยนชื่อมั่วซั่ว" ซึ่งเป็นแนวปฏิบัติมาตรฐานที่มืออาชีพเขาทำกันในโปรเจกต์จริง
กลยุทธ์การ Retry: เครื่องมือช่วยชีวิตไม่ใช่ทางแก้ปัญหา
การทำ Retry Policy หรือการสั่งให้ระบบลองรันซ้ำเมื่อเทสต์พัง เป็นดาบสองคมที่มือใหม่ต้องระวัง การรันซ้ำ (Retry) มีประโยชน์ในกรณีที่เกิดเหตุการณ์ชั่วคราว เช่น เน็ตหลุดหรือ Server ตอบสนองช้า แต่หากคุณใช้มันเพื่อกลบเกลื่อนโค้ดที่บั๊กจริงๆ มันจะกลายเป็นการซ่อนปัญหาไว้ใต้พรมแทน
กฎเหล็กคือให้ตั้งค่า Auto-retries ไว้ต่ำเพียง 1-2 ครั้งเท่านั้น และต้องมีการเก็บ Log (บันทึกการทำงาน) ทุกครั้งที่มีการ Retry เพื่อให้คุณสามารถย้อนกลับมาตรวจสอบได้ว่า ทำไมเทสต์ตัวนี้ถึงต้องรันซ้ำบ่อยกว่าตัวอื่น ซึ่งบ่อยครั้งมันคือสัญญาณเตือนว่ามีปัญหาเชิงโครงสร้างรอให้คุณเข้าไปแก้
การตั้งค่า Retry ใน playwright.config.ts แบบที่เหมาะสม:
import { defineConfig } from '@playwright/test';
export default defineConfig({
// รันซ้ำสูงสุดแค่ 2 ครั้งถ้าเทสต์พัง
retries: 2,
// การรันแบบขนาน (Parallelism) เริ่มต้นให้จำกัดไว้ที่ 2 ก่อน
workers: 2,
});
การตั้ง workers: 2 คือการจำกัดให้ระบบรันเทสต์พร้อมกันเพียง 2 ตัว เพื่อป้องกันไม่ให้คอมพิวเตอร์หรือ Server ของคุณทำงานหนักจนเกินไป (Resource Exhaustion) ซึ่งเป็นสาเหตุหนึ่งที่ทำให้เทสต์พังเพราะเครื่องรันไม่ไหวครับ
การวัดผลและเกณฑ์ความเสถียร: รู้ได้อย่างไรว่าเทสต์เรา "ป่วย"
เราไม่สามารถปรับปรุงสิ่งที่เราไม่ได้วัดผล ดังนั้นคุณควรสร้าง Flakiness Thresholds หรือเกณฑ์วัดความเสถียรของเทสต์ โดยใช้กรอบเวลา 7 วันเป็นตัวชี้วัดหลัก เพื่อดูว่าระบบทดสอบของคุณมีสุขภาพดีแค่ไหน การมีตัวเลขที่ชัดเจนจะช่วยให้คุณตัดสินใจได้ว่าเมื่อไหร่ควรหยุดพักเพื่อแก้เทสต์
นี่คือตารางการประเมินสถานะที่คุณนำไปใช้ได้จริง:
- Acceptable (< 1%): ระบบปกติ เทสต์มีความน่าเชื่อถือสูง
- Warning (1% – 5%): เริ่มมีความไม่เสถียร ต้องรีบเข้าไปตรวจสอบหาสาเหตุ
- Critical (> 5%): ระบบพังหนัก ต้องหยุดการ Deploy ทันทีจนกว่าจะแก้เทสต์ให้เสถียร
การมี Dashboard หรือรายงานสรุปผล (เช่น Allure Report) ที่แสดงจำนวนครั้งที่รันผ่านและจำนวนครั้งที่ต้อง Retry จะช่วยให้คุณเห็นแนวโน้ม (Trends) ว่าเทสต์ตัวไหนกำลังจะกลายเป็นปัญหาในอนาคตก่อนที่มันจะสายเกินไป
สรุป: เปลี่ยนจากมือใหม่ที่ "ลุ้น" เป็นมือโปรที่ "มั่นใจ"
การจัดการกับ Flaky Tests คือการฝึกนิสัยให้เป็นโปรแกรมเมอร์ที่ละเอียดรอบคอบ คุณไม่ได้แค่เขียนโค้ดให้ทำงานได้ แต่คุณกำลังสร้าง ระบบนิเวศของการพัฒนา ที่ทุกคนในทีมสามารถวางใจได้ว่า ถ้าผลทดสอบเขียว (ผ่าน) ก็คือผ่านจริงๆ ไม่ใช่แค่ความบังเอิญ
ลองนำแนวทางนี้ไปปรับใช้ในโปรเจกต์เล็กๆ ของคุณดูครับ เช่น เริ่มจากการตั้งค่า retries ให้ต่ำ, เลือกใช้ data-test-id แทนการอ้างอิงตำแหน่งแบบมั่วๆ และหมั่นเช็คผลรันเทสต์ทุกสัปดาห์ หากคุณทำได้ตามนี้ คุณจะพบว่าตัวเองทำงานได้เร็วขึ้น เพราะไม่ต้องเสียเวลามานั่งไล่แก้บั๊กที่ไม่ได้เกิดจากโค้ดตัวเอง
จำไว้ว่า ความเชื่อมั่นคือสิ่งล้ำค่าที่สุดในทีมพัฒนา การที่เพื่อนร่วมงานหรือหัวหน้าของคุณเห็นว่าระบบทดสอบของคุณแม่นยำและเสถียร มันจะช่วยสร้างชื่อเสียงที่ดีให้กับคุณในฐานะโปรแกรมเมอร์ที่ใส่ใจในรายละเอียดและคุณภาพของงานอย่างแท้จริงครับ
ที่มา: Taming Test Flakiness: A Practical Guide to Retries, Parallelism, and Stability Thresholds — DEV Community