ในการพัฒนาแอปฯ Android ยุคใหม่ ทีมพัฒนาต่างหันมาพึ่งพาการทดสอบ UI แบบอัตโนมัติมากขึ้นเรื่อยๆ ตามความซับซ้อนของแอปฯ ที่เพิ่มขึ้น อย่างไรก็ตาม มีส่วนหน้าจอผู้ใช้ (UI) บางตัวที่ดูเหมือนจะสร้างง่าย แต่กลับทดสอบได้ยากอย่างน่าประหลาด คุณสมเกียรติ กิจวงศ์วัฒนะ Staff Software Engineer จาก LINE MAN Wongnai ได้มาแบ่งปันข้อมูลเชิงลึกเกี่ยวกับตัวการสำคัญที่มักทำให้การทดสอบรวนอยู่บ่อยครั้ง นั่นก็คือ Snackbar นั่นเอง
วิธีการเขียน Snackbar ทั่วไปใน Jetpack Compose มักจะใช้ Scaffold, SnackbarHost และ SnackbarHostState ซึ่งวิธีนี้ทำงานได้สมบูรณ์แบบสำหรับผู้ใช้งานทั่วไป แต่พอนำไปรันในสภาพแวดล้อมระบบอัตโนมัติอย่าง CI/CD มันมักจะเกิดปัญหาเรื่อง Timing ตามมา
คุณลองนึกถึงภาพขั้นตอนการเก็บคูปองดู
ในการทดสอบ UI การตรวจสอบข้อความที่สองมักจะล้มเหลว เพราะข้อความแรกยังไม่ทันหายไปจากหน้าจอเลย นักพัฒนาหลายคนจึงมักเลือกที่จะตัดการตรวจสอบทิ้งเพียงเพื่อให้การทดสอบผ่านไปได้ แต่นั่นหมายถึงการยอมลดคุณภาพของงานลง และส่งผลต่อประสบการณ์ของผู้ใช้อย่างหลีกเลี่ยงไม่ได้
เพื่อขจัดปัญหาการทดสอบที่รวนจนทำงานต่อได้ลำบาก เราจึงควรเปลี่ยนมาใช้โครงสร้างแบบสองส่วนที่ช่วยให้เราควบคุมพฤติกรรมของ Snackbar ในการทดสอบได้ด้วยตัวเอง นั่นคือการใช้ Controller เพื่อสั่งปิดข้อความ และ Container เพื่อจัดการระยะเวลาการแสดงผล
วัตถุประสงค์: ทำหน้าที่เป็นตัวลงทะเบียนเพื่อเก็บอินสแตนซ์ของ SnackbarHostState ที่ถูกสร้างขึ้นตอนแอปฯ ทำงาน เพื่อให้เราสามารถสั่งปิดข้อความ (Dismiss) ผ่านโค้ดการทดสอบได้โดยตรง
ฟังก์ชันหลัก:
registerSnackbarHostState: เพิ่ม State เข้าไปในระบบติดตาม
dismissCurrentSnackbar: สั่ง dismiss() Snackbar ที่กำลังแสดงอยู่ด้วยตนเอง
clearAllSnackbarHostStates: เคลียร์ข้อมูลหลังจบการทดสอบเพื่อป้องกันปัญหาหน่วยความจำรั่ว
เพื่อให้โค้ดที่ใช้งานจริง (Production code) ยังคงสะอาดอยู่ Provider ตัวนี้จะใช้ Java Reflection เพื่อเช็กว่ามี Test Controller พร้อมใช้งานหรือไม่
หากแอปฯ รันอยู่ในสภาพแวดล้อมการทดสอบ มันจะลงทะเบียน SnackbarHostState กับ Controller โดยอัตโนมัติ
ใช้ CompositionLocalProvider เพื่อให้ State นี้ถูกเรียกใช้งานได้ง่ายทั่วทั้งโครงสร้าง UI (UI Tree)
คอมโพเนนต์ตัวนี้จะแก้ปัญหาเรื่องระยะเวลาการแสดงผล ในการใช้งานจริง 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 จะอยู่นิ่งๆ ไม่เกิดปัญหา ไปเป็นการมอบเครื่องมือให้นักพัฒนาสั่งการมันได้ดั่งใจ