มาทำ UI Test สำหรับ Android กันเถอะ

สำหรับการเป็น Android Developer แล้ว สิ่งหนึ่งที่ขาดไม่ได้ในการเขียน Android ก็คือการเขียน Automated test ในส่วนของ UI เพราะว่าการที่เจ้า application ที่เราเขียนจะทำงานถูกต้องได้ เราก็ต้องตรวจสอบในส่วนของการแสดงผลบนหน้าจอด้วยว่าถ้ากดตรงนี้หน้าจอควรแสดงหน้าไหนต่อไป หรือถ้าพิมพ์ค่านี้ส่งไปหน้าจอควรแสดงข้อความว่าอะไรออกมา ซึ่งการจะเขียน Test สำหรับการตรวจสอบในส่วนนี้ เราสามารถนำ Testing Support Library ที่ชื่อว่า Espresso กับ UI Automator มาใช้งานได้

การใส่ Espresso กับ UI Automator ลงใน Project

โดยปกติแล้วถ้าใช้ Android Studio สร้าง project ใหม่ขึ้นมา จะใส่ lib ของ Espresso ลงใน build.gradle แล้ว แต่ถ้ายังไม่มีให้ใส่เพิ่มลงไปให้ครบตามนี้ (ถ้าไม่จำเป็นต้องใช้ UI Automator ก็ไม่ต้องใส่ลงไป ซึ่งจะกล่าวในส่วนต่อ ๆ ไปว่าการใช้งานแบบไหนถึงจำเป็นต้องใช้ library ตัวนี้)

dependencies {
androidTestCompile 'com.android.support.test:runner:0.4'
// Set this dependency to use JUnit 4 rules
androidTestCompile 'com.android.support.test:rules:0.4'
// Set this dependency to build and run Espresso tests
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
// Set this dependency to build and run UI Automator tests
androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
}

หมายเหตุ : ตรวจสอบเวอร์ชันใหม่ล่าสุดได้ใน Testing Support Library

Concept การเขียน UI test

การเขียน UI test มี 3 ขั้นตอนใหญ่ ๆ คือ

  1. Find : หาตัว view ที่เราจะสั่งการหรือตรวจสอบ
  2. Perform : สั่งการทำงานให้กับ view ตัวนั้น
  3. Check : ตรวจสอบการแสดงผลของ view

สมมติว่าเรามีหน้าจอที่ประกอบไปด้วย TextView, EditText และ Button อย่างละหนึ่งตามรูปด้านล่าง เมื่อเราพิมพ์ข้อความอะไรลงไปใน EditText และกด Button ข้อความของ TextView จะเปลี่ยนเป็นข้อความที่เราใส่เราไปใน EditText โดย UI test ที่จะเขียนให้เริ่มจาก

  1. พอเข้ามาหน้านี้ให้ตรวจสอบก่อนว่ามี TextView, EditText และ Button แสดงขึ้นมา และตรวจสอบด้วยว่า TextView เป็นคำว่า Hello และ Button สามารถกดได้
  2. ใส่ข้อความลงใน EditText ว่า test จากนั้นกด Button
  3. ข้อความที่ TextView เปลี่ยนจาก Hello เป็น test

code ที่ได้จะออกมาดังนี้

//Check textView, editText and button is diaplayed on the screen and 
//text in textView is "Hello" and button is clickable
onView(withId(R.id.textView))
.check(matches(isDisplayed()))
.check(matches(withText("Hello")));
onView(withId(R.id.editText))
.check(matches(isDisplayed()));
onView(withId(R.id.button))
.check(matches(isDisplayed()))
.check(matches(isClickable()));
//Add "test" to editText and click button
onView(withId(R.id.editText))
.perform(replaceText("test"));
onView(withId(R.id.button))
.perform(click());
//Check text in textView is "test"
onView(withId(R.id.textView))
.check(matches(withText("test")));

ถ้า view ตัวไหนมีการเรียกใช้ซ้ำบ่อย ๆ เราก็สามารถเรียกเก็บเป็น object แบบนี้ไว้ได้

//Find
ViewInteraction textView = onView(withId(R.id.textView));
ViewInteraction editText = onView(withId(R.id.editText));
ViewInteraction button = onView(withId(R.id.button));
//Check
textView.check(matches(isDisplayed()))
.check(matches(withText("Hello")));
editText.check(matches(isDisplayed()));
button.check(matches(isDisplayed()));
button.check(matches(isClickable()));
//Perform
editText.perform(replaceText("test"));
button.perform(click());
//Check
textView.check(matches(withText("test")));

ถ้า UI test ที่ต้องการเขียนมีความซับซ้อนมากขึ้นก็สามารถนำคำสั่งอื่น ๆ มาลองใช้ได้ ทางตัวเว็บของ Espresso เอง ก็ได้ทำสรุปคำสั่ง espreeso แบบดูง่าย ๆ มาให้ตาม sheet นี้

ที่มาจาก https://google.github.io/android-testing-support-library/docs/espresso/cheatsheet/

โดยส่วนใหญ่แล้วคำสั่งของ Espresso เองค่อนข้างครอบคลุมสิ่งที่เราจะทำใน UI test พอสมควร แต่ก็มีการเขียน UI test บางอย่างที่ไม่สามารถใช้ Espresso ได้ เช่น การลาก-วาง หรือ UI นอกตัว application ของเรา (พวก Dialog ขอ permission ฯลฯ) ทำให้เราต้องนำ UI Automator มาใช้งานเพิ่มด้วย (แต่ถ้า Espresso เพียงพอต่อการใช้งานแล้ว ก็ไม่จำเป็นต้องนำมาใช้)

วิธีการทำงานของ UI Automator จะคล้ายกับ Espresso เลย ( find หา view ที่ต้องการ แล้วเลือกว่าจะ check หรือ perform กับ view ตัวนั้น) เพียงแต่ว่าก่อนที่มันจะหา view เจอได้ เราต้องประกาศ UiDevice ขึ้นมาก่อน เพื่อให้ตัว UI Automator สามารถรู้ได้ว่า ณ ขณะนี้บนหน้าจอของเรามี view อะไรปรากฎขึ้นมาบ้าง

UiDevice device = UiDevice.getInstance(getInstrumentation());

ซึ่งตรงส่วนนี้ทำให้ UI Automator สามารถมองเห็น view ที่อยู่นอกเหนือจากบน application เราได้ ต่างจาก Espresso ที่จะมองเห็นเฉพาะ view ใน application เราเท่านั้น (เพราะตัว Espresso มันจะไปหา view จาก resource ของเราก่อน แล้วค่อยไปดูว่า properties ที่ถูก render ขึ้นมาบน application ขณะนั้นมีอะไรบ้าง)

พอประกาศ UiDevice ขึ้นมาแล้ว เวลาจะเรียกใช้ View ตัวไหน จะต้องประกาศเป็น object ขึ้นมาก่อนเสมอ โดยมีสองอย่างให้เลือกใช้ คือ UiObject กับ UiObject2

UiObject เมื่อสร้างขึ้นมาแล้ว เวลาถูกเรียกใช้งานมันจะค้นหาบนหน้าจอก่อนทุกครั้งว่า view ที่มี properties ตรงกับมันคือตัวไหน มันก็จะยึด view ตัวนั้นมา check/perform ส่วน UiObject2 จะยึดตาม view ที่ตรงกับมันตอนสร้างขึ้นมาเลย ถ้า view ตัวนั้นหายไปจากหน้าจอ มันก็จะไม่สามารถนำไปใช้งานต่อได้ แม้จะมี view ตัวอื่นบนหน้าจอตรงกับ properties ของมันก็ตาม

วิธีการเขียน UiObject กับ UiObject2 จะแตกต่างกัน จากตัวอย่างหน้าจอเดิมสามารถเขียนออกมาได้ตามนี้

  1. UiObject
//Have to throws exception because when view which has properties
//same as created object is not found, it will return
//UiObjectNotFoundException.
@Test
public void mainActivityTest() throws UiObjectNotFoundException{
//Create device
UiDevice device = UiDevice.getInstance(getInstrumentation());
String packageName = device.getCurrentPackageName();
//Find
UiObject textView = device.findObject(
new UiSelector()
.resourceId(packageName+":id/textView"));
UiObject editText = device.findObject(
new UiSelector()
.resourceId(packageName+":id/editText"));
UiObject button = device.findObject(
new UiSelector()
.resourceId(packageName+":id/button"));
//Check
Log.d("Uitest", String.valueOf(textView));
Assert.assertEquals(textView.getText(),"Hello");
Assert.assertTrue(editText.exists());
Assert.assertTrue(button.exists());
Assert.assertTrue(button.isClickable());
//Perform
editText.setText("test");
button.click();
//Check
Assert.assertEquals(textView.getText(),"test");
}

2. UiObject2

@Test
public void mainActivityTest(){
//Create device
UiDevice device = UiDevice.getInstance(getInstrumentation());
String packageName = device.getCurrentPackageName();
//Find
UiObject2 textView = device.findObject(By.res(packageName,"textView"));
UiObject2 editText = device.findObject(By.res(packageName,"editText"));
UiObject2 button = device.findObject(By.res(packageName,"button"));
//Check (Have not to check exists because if view is not found
//when initialize the object, it will return error.)
Log.d("Uitest", String.valueOf(textView));
Assert.assertEquals(textView.getText(),"Hello");
Assert.assertTrue(button.isClickable());
//Perform
editText.setText("test");
button.click();
//Check
Assert.assertEquals(textView.getText(),"test");
}

จากตัวอย่าง code ที่ยกขึ้นมาจะเห็นว่าตัวอย่างคำสั่งที่ใช้จะยังเบื้องต้นอยู่ เหมาะกับ UI ที่เรียบง่าย แต่สำหรับ UI แบบอื่นที่มีความซับซ้อนมากขึ้นจะต้องนำคำสั่งอื่น ๆ มาใช้เพิ่มเติม ซึ่งเอาไว้พูดต่อไปในบทความหน้า

ภาคต่อ>> มา(ดูตัวอย่างการ)ทำ UI Test สำหรับ Android กันเถอะ