Espresso มันขม ? งั้นเติมนมด้วย PageObject

Dew
Black Lens
Published in
3 min readMar 13, 2017
เติมนม

แอปที่วางโครงสร้างไว้เป็นระบบระเบียบย่อมที่จะง่ายต่อการทดสอบต่อเติมและแก้ไข มีแพทเทิร์นมากมายที่เข้ามาช่วยจัดการการวางโครงสร้างโค้ด ไม่ว่าจะเป็น MVP, MVVM, Clean, Redux บลาๆๆ ท้ายที่สุดก็คงแล้วแต่เราจะเลือกมาใช้ตามความเหมาะสมของงานและทีม

คำถามคือในเมื่อเราต้องการโครงสร้างโค้ดที่ดีที่จัดการได้ง่าย ทำไมเราจะไม่ทำแบบเดียวกันกับส่วนของเทสหละ ?

เราอาจจะเคยได้ยินมาจากหลายแหล่งที่บอกว่าเทสนั้นสำคัญ เราอาจจะเคยเห็นคนที่แนะนำให้เราเขียนเทสและคอยถามเราว่า “วันนี้คุณเขียนเทสแล้วหรือยัง ?” และเราเองก็ตั้งหน้าตั้งตาเขียนเทสโดยหวังว่ามันจะช่วยเพิ่มคุณภาพให้กับแอปของเรา แต่สุดท้ายต้องอย่าลืมว่าเทสที่เราเขียนมันก็คือ “โค้ด” มันเองก็ต้องการการจัดการที่ดีเช่นเดียวกัน ถ้าเราวางโครงสร้างเทสไม่ดี สุดท้ายมันก็ไม่ต่างอะไรกับเลกาซี่โค้ดที่ไม่มีใครอยากแตะหรือหนักกว่านั้นก็ไม่ใช้มันเลย

บทความในตอนนี้เราจะมาทำความรู้จักกับอีกหนึ่งเทคนิคที่จะมาช่วยจัดการเทสเคสของ Espresso UI Testing ให้สามารถเพิ่มเติมแก้ไขได้ง่ายขึ้น ซึ่งเทคนิคที่ว่านี้ก็คือ PageObject (หรือ Robot Pattern ก็แล้วแต่จะเรียกกันนะ)

Why PageObject ?

ปกติเวลาแอนดรอยเดฟอย่างเราๆ เขียนเทสด้วย Espresso เราก็จะสร้างคลาสเทสที่เต็มไปด้วยเทสเคสในกรณีต่างๆ ที่เราจะทดสอบเต็มไปหมด แล้วถ้าเราดูในแต่ละเทสเคสสิ่งที่เราจะเห็นก็คือโค้ดหรือฟังก์ชันที่เป็นของ Espresso หรือ Matcher ปะปนอยู่ในนั้น เช่นพวกฟังก์ชัน onView, check, perform, withId, withText, isDisplayed เป็นต้น

@Test
public void greeterSaysHello() {
onView(withId(R.id.name_field))
.perform(typeText("Steve"));
onView(withId(R.id.greet_button))
.perform(click());
onView(withText("Hello Steve!"))
.check(matches(isDisplayed()));
}
โค้ดตัวอย่างจาก https://google.github.io/android-testing-support-library/docs/espresso/

จากตัวอย่างด้านบนเราจะเห็นว่าฟังก์ชัน greeterSaysHello() ที่ไว้ใช้ทดสอบว่าแอปตอบ Hello กับชื่อที่ยูสเซอร์ใส่ไปได้ถูกต้องหรือเปล่า ถ้ามองผ่านๆ ฟังก์ชันนี้ก็ไม่ได้มีปัญหาอะไร แต่ถ้าเราสังเกตดูเราจะพบว่าเนื้อหาในฟังก์ชันนี้เป็น “low-level details” เพราะเราต้องระบุว่าให้ไปใส่ชื่อใน View ที่มีไอดีเป็น R.id.name_field จากนั้นกดปุ่มที่มีไอดีเป็น R.id.green_button แล้วไปเช็คว่า View ที่มีข้อความ Hello Steve! กำลังโชว์อยู่หรือเปล่า

จะดีกว่าไหมถ้าเทสเคสของเราเป็น “high-level details” ที่เมื่อเราอ่านแล้ว มันบอกเราว่า “What is being tested” ไม่ใช่ “How to test it” เช่น

@Test
public void greeterSaysHello() {
greeterPage.sayHello("Steve")
.expectGreetingMessage("Hello Steve!");
}

จะเห็นว่าเทสเคสด้านบนไม่มีอะไรที่เกี่ยวกับ Espresso หรือ Matcher ให้เห็นเลย สิ่งที่ฟังก์ชันนี้บอกเราก็แค่ว่าหน้า greeterPage ต้องตอบว่า “Hello Steve!” ถ้าเราใส่คำว่า “Steve” เข้าไปก็แค่นั้น

ถามว่าแล้ว greeterPage ทำงานยังไง คำตอบคือ greeterPage ซ่อนความซับซ้อนของ Espresso และ Mathcher ต่างๆ มากมายไว้ภายใต้อินเตอร์เฟสที่แสนเรียบง่ายและสวยงามไว้ หรือพูดอีกอย่างว่า greeterPage ทำ low-level details ส่วนเราก็แค่เขียนเทสเคสที่เป็น high-level details ไป

class GreeterPage {... โค้ดส่วนอื่นๆ ที่ละไว้  public GreeterPage sayHello(String name) {
onView(withId(R.id.name_field))
.perform(typeText(name));
onView(withId(R.id.greet_button))
.perform(click());
return this;
}
public GreeterPage expectGreetingMessage(String expectedMessage) {
onView(withText(expectedMessage))
.check(matches(isDisplayed()));
return this;
}
... โค้ดส่วนอื่นๆ ที่ละไว้}

จากโค้ดของคลาส GreeterPage ด้านบนจะเห็นว่าแต่ละฟังก์ชันก็เรียกใช้ Espresso และ Matcher ปกติ เพิ่มเติมอีกนิดโดยเราให้แต่ละฟังก์ชันคืนค่า this ซึ่งเป็น instance ของ GreeterPage ออกมาเพื่อที่ฝั่งคนเรียกจะสามารถใช้งานแบบ Fluent interface ได้อย่างงดงาม

สรุปแล้วหลักการของเทคนิคนี้ก็แค่เราต้องแยกส่วนของ “Test Specs” ออกจาก “Test Implementation”

คราวนี้เทสเคสของเราก็จะอ่านง่ายขึ้น คนที่มาอ่านเทสเคสของเรา (ซึ่งอาจจะเป็น QA) สามารถมาอ่านทำความเข้าใจได้โดยไม่ต้องรู้เรื่อง Espresso หรือในกรณีที่เรามีเอกสาร test specs อยู่เราก็สามารถแปลงมาเป็นโค้ดได้แทบจะตรงตัว

การเพิ่มเติมหรือแก้ไขเทสเคสก็สามารถทำได้ง่ายตราบใดที่ในส่วนของ UI layout จริงๆ ไม่ได้เปลี่ยน (ในกรณีที่ UI เปลี่ยนก็เลี่ยงไม่ได้ที่เราจะต้องมาแก้ PageObject) หรือแม้กระทั่งถ้าเราอยากเปลี่ยนไปใช้เทสเฟรมเวิร์คตัวอื่นที่ไม่ใช่ Espresso เราก็สามารถทำได้โดยไปแก้ที่ PageObject เพียงที่เดียว ตัวเทสเคสก็ไม่กระทบไปด้วย

Sample App

ผมได้เตรียมแอปตัวอย่างเป็นหน้า Login แบบโง่ๆ เรียบๆ ยังไม่ได้แต่งหน้าให้สวยงาม หน้านี้มีลอจิกในการตรวจสอบแพทเทิร์นของอีเมล์, ตรวจสอบ empty field และจำลองการล็อกอินด้วย Handler ของ Android

ตัวอย่างโค้ดจากคลาส LoginTest

... โค้ดส่วนอื่นๆ ที่ละไว้@Test
public void loginSuccess() {
loginPage.login(EMAIL, PASSWORD)
.expectStatusWithMessage(STATUS_SUCCESS);
}

@Test
public void emptyEmailAndPassword() {
loginPage.login("", "")
.expectEmptyEmailErrorMessage(ENTER_EMAIL)
.expectEmptyPasswordErrorMessage(ENTER_PASSWORD);
}
... โค้ดส่วนอื่นๆ ที่ละไว้

ตัวอย่างโค้ดจากคลาส LoginPage

... โค้ดส่วนอื่นๆ ที่ละไว้public LoginPage login(String email, String password) {
return enterEmail(email)
.enterPassword(password)
.tapLoginButton()
.waitForLoginStatus();
}

private LoginPage enterEmail(String email) {
onView(withId(R.id.textInputEditTextEmail))
.perform(typeText(email));
return this;
}
... โค้ดส่วนอื่นๆ ที่ละไว้

ถ้าสนใจลองไปโหลดมาดูได้จาก Githup repo ด้านล่างนี้เลยครับ

ส่งท้าย

อยู่ที่เราจะออกแบบให้ PageObject มีหน้าตาเป็นยังไงจะทำให้ใช้ง่ายหรือซับซ้อนแค่ไหน เราแค่ต้องเข้าใจหลักของมันที่แยกส่วนของ test specs และ test implementation ออกจากกัน ง่ายๆ เพียงแค่นั้น

นอกจากนี้เรายังสามารถนำเทคนิค PageObject ไปใช้กับ UI Testing ของแพลตฟอร์มอื่นๆ ก็ได้ ไม่จำกัดอยู่แค่แอนดรอย หากสนใจศึกษาเพิ่มเติมแนะนำเข้าไปดูต่อตามลิงค์ด้านล่างได้เลยครับ

สวัสดีครับ

--

--