มีอะไรใหม่เกี่ยวกับ Testing จากงาน Google I/O 2019 อัพเดตล่าสุดที่นี่!

Traitanit Huangsri
LINE Developers Thailand
5 min readMay 12, 2019

ก็จบไปแล้วนะครับ สำหรับงาน Google I/O 2019 งาน Developer Conference ใหญ่ประจำปีของ Google ซึ่งก็มี Technology ใหม่ๆ ให้เราที่เป็นนักพัฒนาได้ติดตามอัพเดตและนำไปใช้กับ Project ของตัวเองมากมายเลยนะครับ

ในบทความนี้ ส่วนตัวผมในฐานะที่ทำงานทางด้าน Software Testing มานาน ก็จะขอสรุปรวบยอดว่ามีอะไรใหม่ที่เกี่ยวข้องกับการทำ Testing ในปีนี้ที่ทาง Google ได้ Update ไปแล้วบ้าง

Android Testing Update

ขอเริ่มต้นจาก Mobile Platform ที่มีคนใช้มากที่สุดในโลกอย่าง Android กันก่อนครับ ในงาน Google I/O ปีนี้ มี Session ที่ Update เกี่ยวกับ Android Testing โดยใช้ชื่อหัวข้อว่า “Build Testable Apps for Android” โดย Jonathan Gerrish ซึ่งเป็น คนที่ดูแลเกี่ยวกับ Android Testing มายาวนาน ซึ่งใน Session นี้ได้มีการแนะนำวิธีการเขียนแอพ Android ยังไงให้สามารถเขียน Test ได้ง่ายๆ และมีการเลือกใช้ Testing Strategy ที่เหมาะสม (การทำ Test ในที่นี้หมายถึง Automated Testing ทั้งหมดนะครับ)

ผมคิดว่าหลายๆ องค์กรน่าจะเคยเจอปัญหาคล้ายๆ กันก็คือ เรามีแอพ Android ที่เขียน Test ยากมาก หรือเขียน Test ไม่ได้เลย และทำให้ต้องใช้ Effort และเวลาในการทำ Manual Testing ค่อนข้างมาก ซึ่งเกิด Cost ที่เพิ่มขึ้นเรื่อยๆ ยิ่งแอพมี Feature มากขึ้น โค้ดมี Complexity มากขึ้นก็ยิ่งทำให้ใช้เวลา Test มากขึ้นไปอีก

แล้ว Testing Strategy ที่ดีควรจะเป็นยังไง ?

Jon แนะนำว่าหลักในการทำ Test ที่ดี เพื่อให้เกิด Test ที่ Maintainable สามารถใช้งานได้ในระยะยาวนั้นมีอยู่หลักการอยู่ด้วยกัน 3 ข้อคือ

  1. Scope คือการกำหนด Scope ว่าเราต้องเขียนเทสมากน้อยแค่ไหน จะเลือกเทสเป็นทีละ function, เทสแค่บางส่วนของแอพ หรือจะเทสทุกอย่างทั้งหมดของแอพเลย
  2. Speed คือเทสควรจะต้องรันเร็วได้ขนาดไหน แบบไหนที่เรียกว่าเร็วพอถึงจุดที่เราพอใจ
  3. Fidelity คือการเขียนเทส ต้องเทสให้เหมือน User ใช้งานจริงขนาดไหน? เช่นถ้าแอพต้องมี Network Request ไปหา Server เพื่อนำ Data มา Display บน UI จะต้องเทสแบบต่อ Server จริงๆ หรือทำเป็น Mock Data ก็พอ ซึ่งถ้า Fidelity สูง คือเทสเหมือนจริงมากก็ยิ่งใช้เวลารันนานและทำให้เกิด Flaky Test ได้ง่าย แต่ถ้า Fidelity ต่ำ ก็จะทำให้เทสได้เร็ว แต่ก็อาจจะไม่เหมือนการใช้งานจริง 100%

Speed กับ Fidelity คือสิ่งที่เราต้อง Trade-off กันและหาจุด Balance ที่เหมาะสมสำหรับ Project ของเรา ความยากมันอยู่ตรงนี้

การ Balance Testing Strategy ทั้งสามข้อให้เหมาะสมนั้น Jon แนะนำว่าเราควรจะ Apply หลักการของ Testing Pyramid มาใช้กับการทำเทสของเรา ซึ่งจะช่วยให้เราตัดสินใจได้ว่าควรทำเทสตรงไหนมาก ตรงไหนน้อย

Testing Pyramid credit: https://codelabs.developers.google.com

Testing Pyramid คือ Concept การแบ่ง Level ให้เป็นสัดส่วนที่แตกต่างกัน เพื่อให้เกิดการทำเทสที่เหมาะสมกับ Testing Strategy ที่เราต้องการ

  • Unit Tests: คือการเทสแบบ Focus เฉพาะจุดที่เราสนใจ เช่นเทส Function ที่มี Business Logic ที่สำคัญ ซึ่ง Unit Test เป็น ​Test ที่มี Fidelity ต่ำ คือเทสเฉพาะจุด เฉพาะ Function, Class ก็จะไม่เหมือน Test Scenario User ใช้งานจริง แต่แลกมาซึ่งความเร็วที่สามารถรันและให้ผลเทสได้อย่างรวดเร็ว ถ้า Unit Test รันไม่ผ่านก็จะสามารถบอกโค้ดส่วนที่ทำงานผิดพลาดให้เราได้เลยโดยที่ไม่ต้องใช้เวลา Investigate หาปัญหานานๆ ซึ่งตามหลักแล้วใน​ Project เราควรจะมี Unit Test ให้มากที่สุด
  • Integration Tests: คือการเทสเพื่อดู Interaction ระหว่าง Class หลายๆ Class เพื่อดูว่ามันทำงานเป็นไปอย่างที่เราต้องการไหม โดยอาจจะเน้นเป็นการเทสแบบ Focus แค่ Feature ใด Feature หนึ่ง เช่นสมมติว่าแอพเรามี Feature ที่สามารถ Save Data บางอย่างของ User ลง DB ได้ เราก็เทสแค่การ Save Data แล้วก็ดูว่ามัน Save ได้ถูกต้องไหม เป็นต้น ซึ่ง Integration Test นั้นมี Fidelity ที่สูงกว่า Unit Test คือเริ่มใกล้เคียงกับที่ User ใช้มากขึ้น แต่ก็จะรันเทสได้ช้ากว่า
  • End-to-End Tests: คือการเทสเพื่อดูการทำงานร่วมกันของทุกๆ Features ในแอพของเราว่ามันทำงานร่วมกันทั้งหมดได้ไหม (Full Stack) มีการทำ Test Scenario ที่เหมือน User ใช้งานจริง ตั้งแต่ทำ Action บน UI ไล่ไปจนถึงปลายทางเช่น DB หรือ Network Request เอา Data จาก Server มาโชว์บน UI เป็นต้น ซึ่ง E2E Test เป็นการเทสที่มี Fidelity สูงสุด คือเหมือน User ใช้งานจริงเลย แต่ก็รันได้ช้าที่สุดเหมือนกันและเกิด Flaky Test ง่ายอีกด้วย ดังนั้นเราจึงควรมี E2E Test ใน Project ของเราให้น้อยที่สุด

สิ่งที่จะช่วยให้เกิด Testing Strategy แบบนี้ได้ก็คือการวาง Architect แอพของเราให้สามารถเขียน Test ได้ง่าย และไม่มี Dependency ระหว่างกันมากจนเกินไป

Monolithic Application

Monolithic Application

แรกเริ่มเดิมทีเราเขียนแอพเล็กๆ ที่มีไม่กี่ Class ไม่กี่ Function ก็คงจะพอเทสได้ไม่ยากนัก เพราะ Feature ยังไม่เยอะ และ Complexity ของโค้ดยังต่ำอยู่ แต่โดยธรรมชาติแล้วแอพเรามักจะมีการ Evolve ตัวเองอย่างรวดเร็ว มี Class ที่เพิ่มขึ้นมากมาย ทำให้เกิด Dependency สูงและทำเทสได้ยากยิ่งขึ้น ไม่สามารถทำเทสแบบ Isolate แยกกันได้เลยทำให้ต้องเขียนเทสเป็น End-to-End Test ทั้งหมด ซึ่งเวลามีการแก้โค้ดของแอพหนึ่งครั้งก็แทบจะต้องแก้เทสใหม่เยอะมาก ต้องแก้ให้ทำงานให้ได้เหมือนเดิมซึ่งต้องแก้หลายจุดเพราะมี Dependency สูง ทำให้ใช้เวลาในการเขียนเทสนาน แถมช้าและ Flaky ได้ง่ายอีกด้วย

Modular Application

เมื่อ App เราโตขึ้นด้วย Feature ที่มากขึ้น Jon แนะนำว่าเราก็ควร Design แอพของเราให้เป็น Modular Architecture คือแบ่ง Module ของแอพเราตาม Feature ต่างๆ ที่เรามี ซึ่งจะช่วยให้เรา Isolate Class แยกออกเป็นโมดูลย่อยๆ ซึ่งมีข้อดีก็คือทำให้เราสามารถเขียนเทสได้ง่าย Test สามารถรันเฉพาะ Module ที่มีการแก้โค้ดได้ ไม่ต้องรันเป็น Flow ยาวๆ ทั้งหมด และแถมยังทำให้ Code ของเรา Compile ได้เร็วขึ้นอีกด้วย ถ้าใครอยากรู้รายละเอียดการ Architect แอพของเราให้เป็นแบบ Modular Architecture ในงานนี้มีการพูดถึงเรื่องนี้เหมือนกัน สามารถดู Session เต็มๆ ด้านล่างนี้ได้เลยครับ

Best Practices for Writing Tests

เมื่อ App Architecture ของเราถูกออกแบบมาให้เหมาะกับการเขียนเทสแล้ว เมื่อเราจะเริ่มเขียนเทส เราก็ควรจะมี Practices ในการเขียนเทสในแต่ละ Level อย่างไรให้สามารถตอบโจทย์ Testing Strategy ที่เราต้องการได้คือ Scope, Speed และ Fidelity และ Jon ยังได้แนะนำให้ใช้ Test Driven Development (TDD) มาใช้กับ Project ของเราด้วย

Red-Green-Refactor เป็น Practice ที่นำมาใช้กับการทำ TDD

การทำ TDD คือการพัฒนา Application โดยการเริ่มต้นด้วยการเขียน Test ก่อน โดยเริ่มต้นเขียน Test ตาม Specification ที่เราต้องการและทำให้รัน Failed ก่อนแล้วค่อยเขียนโค้ดเพื่อทำให้ Test นั้นรันผ่านและใช้งานได้ หลังจากนั้นก็ค่อย Refactor โค้ดให้ดีขึ้นเรื่อยๆ โดยการเขียนเทสก็จะใช้ Approach Top-Down เริ่มต้นจาก E2E Tests และ Decompose ลงไปถึง Unit Test

E2E Tests Concept

End-to-End Tests: เริ่มต้นการเขียนเทสด้วย Specification ที่เป็นการใช้งานจริงของ User รันบน Real/Virtual Device โดยใช้ Dependencies จริงทั้งหมด ซึ่งเป็นการทำเทสแบบ Black Box Testing การทำ E2E Tests จะช่วยให้เรามั่นใจว่าแอพที่เราเขียนนั้นสามารถทำงานได้จริงใน Scenario ที่ User ใช้งาน

E2E Tests คือเทสทั้ง Full Stack ใน App ของเราตั้งแต่ Activity -> Persistent Database
Integration Tests Concept

เมื่อเราเขียนแอพให้ E2E Tests รันผ่านหมดแล้ว เราก็ค่อยๆ เพิ่ม Integration Tests โดยควร Focus ดูการทำงระหว่าง ส่วนย่อยๆ แต่ละอัน ซึ่ง Integration Tests นั้นควรจะ Scalable เพื่อลดปริมาณเทสเคสที่เป็น E2E Tests ให้น้อยลง รวมถึงรันเทสด้วย Data ที่เป็นของจริงและรวมถึง Mock data ด้วยเพื่อให้เทสรันได้เร็วขึ้น ซึ่งลักษณะการเทสแบบนี้จะเรียกว่าเป็นการทำ Grey Box Testing

Integration Tests ระหว่างแต่ละ Layer โดย Fake Data บางส่วน (ตัด Activity ออก) ซึ่งโอเคที่จะทำ
หรือจะ Mock Repository ทั้งก้อนก็ยังได้ (เพราะเด๊วเราไปทำ Unit Test ใน Repository แทน) และทำให้เกิด Scalable

Unit Test: เป็นการเทสที่ Focus ระดับหน่วยย่อยที่สุดของแอพ เช่น Methods, Class โดยเป็นการรันเทสที่รันได้เร็วที่สุด ควรเป็นการรันแบบ Local คือไม่มีส่วนของ ​Android OS มาเกี่ยวข้อง และควรเทสทั้งแบบ Real และ Mock Data และเราก็ควรมองการทำ Unit Test เป็นแบบ Black Box Testing คือเทสแนว Behavior มากกว่าการเทส Implementation แค่ให้ผ่านโค้ดที่เราเขียน

Unit Tests Concept
ตัวอย่าง Unit Test ที่เทสระดับ ​Methods ของ Repository Layer

Android Testing Types

ใน Android นั้นมีลักษณะการเขียนเทสอยู่ 2 แบบคือ

Local Tests: เป็นการเทสที่รันบน Local JVM โดยตัด Android OS ออกไป ซึ่งอาจจะใช้ Testing Library อย่าง Robolectric ช่วยได้ ซึ่ง Local Tests นั้นรันได้เร็ว, Scalable แต่ก็เป็นการเทสที่มี Fidelity ต่ำ ไม่เหมือนการรันบน Device จริง

Instrumentation Tests: คือการรันเทสบน Real/Virtual Devices ซึ่งใกล้เคียงกับการใช้งานจริง (High Fidelity) แต่ก็มีข้อเสียคือช้า และไม่ค่อย Scale

ซึ่งในงาน Google I/O 2018 ปีที่แล้วก็ได้มีการ Announced เรื่อง AndroidX Test ซึ่งเป็น Set ของ Test Library ที่ Support การรันเทสทั้งสองแบบครับ Testing Library ที่ว่าก็คือ Robolectric และ Espresso นั่นเองครับ

Future of Android Testing

Google มีความพยายามในการ Unify Testing Platform ให้เป็นหนึ่งเดียวกัน เราจะเห็นได้ชัดในการทำ Integration Tests ว่าเราสามารถรันได้ทั้งบน Local JVM และ ​Real/Virtual Devices ซึ่ง Google ก็ได้มีการ Announced Project Nitrogen ตั้งแต่งาน Google I/O 2018 ที่ผ่านมา

Project Nitrogen เป็น Unified Android Testing Platform ที่รวมความสามารถในการทำงานร่วมกับระบบ Continuous Integration, การรันเทสทั้งแบบ Local JVM, Instrumentation Tests และการรันเทสบน Cloud Platform อย่าง Firebase Test Lab ไว้ในที่เดียวกัน

Project Nitrogen ตอนนี้มีการเปิด EAP (Early Access Program) ให้ Developers สามารถสมัครขอเข้าไปใช้งานได้แล้ววันนี้ที่ http://bit.ly/nitrogen-eap ครับ

Google ได้ Update Codelab สำหรับ Android Testing ที่เป็นตัวอย่างแอพที่มีการ Design App Architecture แบบ Modular Architecture รวมถึงตัวอย่างการเขียนเทสในแต่ละ Level ที่ Compatible กับ Testing Pyramid Concept สามารถเข้าไปทำ Code Lab ได้ที่ https://codelabs.developers.google.com/codelabs/android-testing/ ได้เลยครับ

สรุป

สิ่งที่สำคัญที่สุดในการที่จะทำให้ Android แอพของเราสามารถเขียนเทสได้ง่ายนั้นก็คือการวาง App Architecture ให้เหมาะสม โดยการทำเป็น Modular Application และเมื่อเริ่มเขียนเทส ก็ควรจะมี Testing Strategy ที่เหมาะสมว่าควรเขียนเทสใน Level ไหนมากน้อย เพื่อให้เกิดการลงทุนที่คุ้มค่า (Return of Investment) ในระยะยาวครับ

ยังมี Update อีกเรื่องนึงที่เกี่ยวข้องกับการทำ Testing จากงาน Google I/O 2019 ซึ่งเกี่ยวข้องกับการทำ Web Automation Test บน Google Chrome ด้วย Puppeteer แต่ผมขอไปเขียนในบทความถัดไปแล้วกันนะครับ ติดตามอ่านได้จากลิ้งค์ด้านล่างนี้ได้เลยครับ Happy Testing!

--

--