เคล็ดลับจัดการ Snackbar ใน Jetpack Compose เพื่อให้ทดสอบ UI อย่างแม่นยำและเชื่อถือได้
ในการพัฒนาแอปฯ Android ยุคใหม่ ทีมพัฒนาต่างหันมาพึ่งพาการทดสอบ UI แบบอัตโนมัติมากขึ้นเรื่อยๆ ตามความซับซ้อนของแอปฯ ที่เพิ่มขึ้น อย่างไรก็ตาม มีส่วนหน้าจอผู้ใช้ (UI) บางตัวที่ดูเหมือนจะสร้างง่าย แต่กลับทดสอบได้ยากอย่างน่าประหลาด คุณสมเกียรติ กิจวงศ์วัฒนะ Staff Software Engineer จาก LINE MAN Wongnai ได้มาแบ่งปันข้อมูลเชิงลึกเกี่ยวกับตัวการสำคัญที่มักทำให้การทดสอบรวนอยู่บ่อยครั้ง นั่นก็คือ Snackbar นั่นเอง
ทำไมการทดสอบ Snackbar แบบมาตรฐานถึงมักจะล้มเหลว
วิธีการเขียน Snackbar ทั่วไปใน Jetpack Compose มักจะใช้ Scaffold, SnackbarHost และ SnackbarHostState ซึ่งวิธีนี้ทำงานได้สมบูรณ์แบบสำหรับผู้ใช้งานทั่วไป แต่พอนำไปรันในสภาพแวดล้อมระบบอัตโนมัติอย่าง CI/CD มันมักจะเกิดปัญหาเรื่อง Timing ตามมา
คุณลองนึกถึงภาพขั้นตอนการเก็บคูปองดู
- การกระทำ: ผู้ใช้กดปุ่ม "เก็บคูปอง"
- Snackbar แรก: แอปฯ แสดงข้อความ "กำลังใช้คูปอง..."
- ปัญหาเรื่องจังหวะ (Race): หากเซิร์ฟเวอร์ตอบกลับเร็วมาก แอปฯ จะพยายามแสดง Snackbar ตัวที่สอง ("เก็บคูปองสำเร็จ!") ก่อนที่ตัวแรกจะหายไป
ในการทดสอบ UI การตรวจสอบข้อความที่สองมักจะล้มเหลว เพราะข้อความแรกยังไม่ทันหายไปจากหน้าจอเลย นักพัฒนาหลายคนจึงมักเลือกที่จะตัดการตรวจสอบทิ้งเพียงเพื่อให้การทดสอบผ่านไปได้ แต่นั่นหมายถึงการยอมลดคุณภาพของงานลง และส่งผลต่อประสบการณ์ของผู้ใช้อย่างหลีกเลี่ยงไม่ได้
การสร้าง Controller และ Container ที่เป็นมิตรต่อการทดสอบ
เพื่อขจัดปัญหาการทดสอบที่รวนจนทำงานต่อได้ลำบาก เราจึงควรเปลี่ยนมาใช้โครงสร้างแบบสองส่วนที่ช่วยให้เราควบคุมพฤติกรรมของ Snackbar ในการทดสอบได้ด้วยตัวเอง นั่นคือการใช้ Controller เพื่อสั่งปิดข้อความ และ Container เพื่อจัดการระยะเวลาการแสดงผล
1. SnackbarUiTestController
-
วัตถุประสงค์: ทำหน้าที่เป็นตัวลงทะเบียนเพื่อเก็บอินสแตนซ์ของ SnackbarHostState ที่ถูกสร้างขึ้นตอนแอปฯ ทำงาน เพื่อให้เราสามารถสั่งปิดข้อความ (Dismiss) ผ่านโค้ดการทดสอบได้โดยตรง
-
ฟังก์ชันหลัก:
-
registerSnackbarHostState: เพิ่ม State เข้าไปในระบบติดตาม
-
dismissCurrentSnackbar: สั่ง dismiss() Snackbar ที่กำลังแสดงอยู่ด้วยตนเอง
-
clearAllSnackbarHostStates: เคลียร์ข้อมูลหลังจบการทดสอบเพื่อป้องกันปัญหาหน่วยความจำรั่ว
-
2. SnackbarHostStateProvider
เพื่อให้โค้ดที่ใช้งานจริง (Production code) ยังคงสะอาดอยู่ Provider ตัวนี้จะใช้ Java Reflection เพื่อเช็กว่ามี Test Controller พร้อมใช้งานหรือไม่
-
หากแอปฯ รันอยู่ในสภาพแวดล้อมการทดสอบ มันจะลงทะเบียน SnackbarHostState กับ Controller โดยอัตโนมัติ
-
ใช้ CompositionLocalProvider เพื่อให้ State นี้ถูกเรียกใช้งานได้ง่ายทั่วทั้งโครงสร้าง UI (UI Tree)
3. SnackbarContainer
คอมโพเนนต์ตัวนี้จะแก้ปัญหาเรื่องระยะเวลาการแสดงผล ในการใช้งานจริง Snackbar จะมีระยะเวลาแบบสั้นหรือยาว แต่เมื่อฟังก์ชัน isRunningUiTest() เป็นจริง Container จะเปลี่ยนระยะเวลาให้เป็นแบบ Indefinite (แสดงค้างไว้ตลอด) วิธีนี้ช่วยให้มั่นใจว่า Snackbar จะอยู่บนหน้าจอนานพอที่ตัวทดสอบจะตรวจสอบความถูกต้องได้ และไม่หายไปก่อนเวลาที่มันควรจะอยู่ถึง
การนำขั้นตอนที่เอื้อต่อการทดสอบไปใช้งาน
ด้วยโครงสร้างแบบนี้ การทดสอบ UI ของคุณจะคาดเดาผลได้และอ่านง่ายขึ้นมาก แทนที่จะต้องมานั่งลุ้นเรื่องจังหวะเวลา โค้ดการทดสอบของคุณจะทำงานตามรูปแบบที่ชัดเจนคือ "ตรวจสอบ แล้วจึงสั่งปิด" (Verify-and-dismiss pattern)
Kotlin
@Test |
ข้อควรระวังสำหรับการนำไปใช้จริง
แม้ว่าวิธีนี้จะมีประสิทธิภาพมาก แต่มี 2 จุดสำคัญที่ต้องระลึกไว้เสมอ
1. อย่าให้โค้ดทดสอบหลุดไปในงานจริง: ตรวจสอบให้แน่ใจว่า SnackbarUiTestController อยู่ในโฟลเดอร์ androidTest เท่านั้น หากหลุดไปอยู่ใน Production ตัว Snackbar ของคุณอาจจะค้างอยู่บนหน้าจอผู้ใช้จริงตลอดกาลได้
2. ความซับซ้อน: ในแอปฯ ที่ซับซ้อน Snackbar จากคอมโพเนนต์อื่นอาจจะโผล่ขึ้นมาแทรกระหว่างการทดสอบได้ ดังนั้นควรใช้เมธอด @After เพื่อเคลียร์ Host States ทั้งหมดระหว่างจบแต่ละการทดสอบเสมอ
เน้นความเสถียร
มากกว่าความเรียบง่าย
การบังคับให้ Snackbar แสดงค้างไว้ (Indefinite duration) ระหว่างการทดสอบ และการใช้ Controller ส่วนกลางเพื่อสั่งปิดข้อความ จะช่วยเปลี่ยนการทดสอบ Snackbar ที่เคยรวนให้กลายเป็นส่วนที่เสถียรที่สุดในชุดทดสอบของคุณ วิธีนี้ก้าวข้ามจากการหวังว่า UI จะอยู่นิ่งๆ ไม่เกิดปัญหา ไปเป็นการมอบเครื่องมือให้นักพัฒนาสั่งการมันได้ดั่งใจ
คุณสมเกียรติ กิจวงศ์วัฒนะ
Staff Software Engineer - Android at LINE MAN Wongnai
คุณสมเกียรติเป็นนักพัฒนา Android ที่มีความหลงใหลในสายงานนี้มานานกว่า 10 ปี มีประสบการณ์ทั้งในด้านการพัฒนาซอฟต์แวร์ระดับองค์กร และเป็นผู้ที่มีส่วนร่วมขับเคลื่อนชุมชน Android ในประเทศไทยอย่างต่อเนื่อง ก่อนหน้านี้เขาเคยทำงานเป็นนักพัฒนาฮาร์ดแวร์ สร้างระบบฝังตัว (Embedded Systems) ที่ทำงานเชื่อมต่อกับอุปกรณ์ Android

