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

Android Nov 9, 2017
https://pixabay.com/en/mri-magnetic-resonance-imaging-2813911/

หลังจากที่เราได้ศึกษาเรื่อง Android Architecture Components ไปพักนึงแล้วนั้น ยังขาดไปเรื่องหนึ่งที่เป็นหัวใจสำคัญในการทำ Android Development นั่นคือการทำ Unit Test นั่นเอง เพื่อให้เราสามารถตรวจสอบความถูกต้องของโค้ดที่เราเขียน ตรวจสอบการทำงานภายในแอป ตรวจสอบในด้าน performance เช่น crash ต่างๆที่เกิดขึ้น

ในแอปฟังใจเองก็มีการทำ Testing หลายอย่าง ทั้ง Unit Test โดยตัว developer เองและ Beta Test จาก Fabric ปล่อยแอปเพื่อลองใช้กันเองในทีมก่อน

ดังนั้นการ Testing จึงเป็นเรื่องสำคัญในการพัฒนาแอปปิเคชั่นนึงไปถึงมือ user ให้ user ไม่ว่าเรา เอ้ยยย ให้ user happy และเพื่อแบรนด์ของเราด้วย เย่เย้

คงไม่มี Mobile Developer คนไหน อยากโดน User ด่าว่าปล่อยแอปที่เต็มไปด้วย bug และ crash หรอกนะ จริงไหม?

ก่อนอื่นมาปรับทัศนคติ เอ้ยยย ปรับพื้นฐานกันก่อน ว่า Unit Test คืออะไร
(ตอนแรกว่าจะรวม ไปๆมาๆเริ่มเยอะไปแล้ว เลยเขียนบล็อกแยกอีกอันดีกว่า)

มาทำความรู้จัก Unit Test สำหรับ Android Developer กันเถอะ
การที่ Android Developer รู้จัก Unit Test ก็เหมือนโอตะ BNK48 ทุกคนที่ต้องรู้จักแคปเฌอ >w< (ส่วนโอชิใครก็อีกเรื่องนึง ;P)

สำหรับคนที่หลงมาอ่านแล้วงงๆว่า Android Architecture Components คืออะไร อ่านสองบล็อกย้อนหลังได้เลยจ้า

Android Architecture Components คืออะไร แบบม้วนเดียวจบ
Android Developer หลายๆคนเริ่มพูดถึงกันแล้วในประเด็นนี้ กับ Architecture Components อยู่วงการนี้ต้องไวนะ ไม่ว่า Android…
เจาะลึกไปถึงชีวิตการทำงานของ Code ใน Repository บน Architecture Component
สำหรับ Android Architecture Component ถือว่าเป็นเรื่องใหม่ และหลายๆคนกำลังศึกษาโครงสร้างนี้ ซึ่งประกอบด้วยหลายๆส่วนงาน ทั้ง View…

มาเริ่มทำ Testing ของ Architecture Components แต่ละตัวกันดีกว่า

ตามปกติแล้วนั้น ส่วนของการ Testing จะซ่อนตัวกันแบบนี้ 
ส่วนใหญ่หลังจากที่เราสร้างโปรเจกเสร็จ

แล้วสำหรับ Architecture Components หล่ะ?

ตาม guideline ของ google และจากที่ได้อ่านมา แบ่งแต่ละส่วนประมาณนี้

อ้างอิงจากบล็อกพี่เอก http://www.akexorcist.com/2017/05/new-things-in-google-io-2017-for-android-developer.html เอามาจดรวมกันในรูปเดียวด้วยดินสอ แล้วเข้าเครื่องแสกน สีจะดูซีดแปลกๆหน่อย ใครเอาไปใช้ให้เครดิตด้วยเน้อ
  • UI Controller [Activity/Fragment with LifecycleOwner] : ส่วนของหน้าตาแอป test ด้วย Android Instrumentation Test หรือทำ UI Test บน Espresso โดย mock-up ViewModel
  • ViewModel : ส่วนที่รับข้อมูลจาก Repository และส่งต่อให้ส่วน UI
    Test ด้วย Android JUnit Test โดย mock-up Repository
  • Repository [LiveData] : ผู้ประสานงานทางข้อมูล Test ด้วย Android JUnit Test โดย mock-up Data Sources
  • Data Sources [Room (SQLite), HTTP client (Retrofit), Content Provider] : พวกที่เชื่อมต่อกับ web service ใช้ MockWebServer ในการตรวจสอบการทำงาน โดย mock-up ตัว server ไว้
  • Model : พวก DAO (Data Access Object) ทั้งหลาย ใช้ Instrumentation test

Guide to App Architecture
This guide is for developers who are past the basics of building an app, and now want to know the best practices and…developer.android.com

มาเริ่มเขียน Testing กัน

ตอนแรกจะลองเอาโปรเจกตัวอย่างตอนที่พูดถึงการทำงานของโค้ดบน Repository มา แต่….เขียน Test เองแล้วพังเยอะ เลยเอาโปรเจกตัวอย่างของ google มาศึกษาเองดีกว่าว่าเขาเขียน Test กันยังไง และเขาได้เขียน Test ครบทุก component เลยนะ เป็น guideline ได้เลยนะ ถ้าทำความเข้าใจโค้ดดีๆ

android/architecture-components-samples
Samples for Android Architecture Components. . Contribute to android/architecture-components-samples development by creating an account on GitHub.

โค้ดชุดนี้ทำอะไร? เป็นแอป search ชื่อ Repository ใน Github จากนั้นเราเข้าไปดูชื่อของเจ้าของ Repository และดูได้ว่า คนนี้อัพอะไรขึ้น Github บ้าง

ดังนั้นหน้าตาโครงสร้าง เป็นประมาณนี้

มาลอง scan และจับกลุ่มคร่าวๆกันดู

เนื่องจากในแต่ละ Component มีหลายไฟล์เนอะ หลักๆจะมีของ Search, Repo, User ขออนุญาติผู้อ่านยกแค่ไฟล์เดียวมาอธิบายในการ Testing ในแต่ละ Component

การตั้งชื่อไฟล์ Test มักจะใช้สูตร <class_name>+“Test” เพื่อให้แยกได้ว่าไฟล์นี้เรา test ของไฟล์ไหน เช่น RepoDaoTest.java

เริ่มทำ Unit Testing ของแต่ละ Component กันดีกว่า

1. UI Controller : <module_name>/src/androidTest/java/

ทำ Instrumentation Test บน Espresso และใช้ Mockito ในการ mock-up สิ่งต่างๆ

โดยเพิ่ม library ทั้งหลาย ที่ build.gradle ของ module

androidTestImplementation "android.arch.core:core-testing:1.0.0-alpha9-1"
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
    exclude group: 'com.android.support', module: 'support-annotations'
})
testCompile 'org.mockito:mockito-core:2.11.0'

มาทำความรู้จัก Mockito กันคร่าวๆก่อนนะ

Mockito คือ เครื่องดื่มชนิดหนึ่ง เป็น cocktail ที่มีส่วนผสมของมินต์ มะนาว นํ้าตาล และแอลกอฮอลเล็กน้อย มีขิงหน่อยๆ

credit : https://pixabay.com/en/mojito-cocktail-drink-beverage-698499/ สูตรก็ตามนี้นะ http://www.friedchillies.com/recipes/detail/mock-ito

เดี๋ยวนะ มันใช่หรอ!

Mockito ชื่อเหมือนเครื่องดื่มชนิดหนึ่งนั่นแหละ และก็เป็นชื่อของ library ที่ใช้ในการทำ Unit Test บน Android โดย stub object ที่เราไม่ได้ test เช่น ในตอนนี้เราจะ test หน้า Activity เราใช้ mockito mock ViewModel ขึ้นมา หรือ object อื่นๆที่เอามาแสดงผล ซึ่งในที่นี้ test การกดแบบต่างๆ แล้วต้องแสดงผลตามที่เราต้องการ

คำสั่งที่ใช้กัน ก็มีประมาณในรูปนี่แหละ

capture from http://site.mockito.org/
  • สร้าง mock-up object โดยใช้คำสั่ง mock() และไส้ในคือ object ที่เราจะทำการ mock-up ขึ้นมา เช่น ViewModel
  • เราใส่พฤติกรรมการทำงานของ object ที่ถูกทำงาน ไว้ใน when()
  • spy() ใช้กับ object จริง ที่ไม่ผ่านการ mock-up ซึ่งในที่นี้ก็ไม่ได้ใช้นะ
  • verify() ใช้กับ function ที่มีการรับค่า argument เข้ามา เพื่ออ่านค่าที่ได้จาก function นั้นๆ แต่ไม่ได้คืนค่าอะไรออกมาแบบนั้นนะ แค่บอกว่าทำได้ไม่ได้แค่นั้น

มาดูโค้ดกันดีกว่า เราเลือกไฟล์ที่ test SearchFragment.java มายกตัวอย่าง

ถามว่าแต่ละอัน test อะไร ดูคลิปประกอบดีกว่าเนอะ ง่ายกว่า 555 มันจะรันไปทีละอัน จากบนไปล่างนั่นแหละ

มีความวูบวาบในการกด UI โดย Espresso เบาๆ

2. View Model : <module_name>/src/test/java/

ใช้ JUnit ในการ test และเราจะต้อง mock ในส่วนของ Repository โดยใช้ Mockito เช้าช่วย ดังนั้นเข้าไปเพิ่ม dependency ของ Mockito ที่ build.gradle ของ module

testCompile 'org.mockito:mockito-core:2.11.0'
มีทริคเล็กน้อยสำหรับการใส่ dependency สำหรับการนำ library ตัวนั้นๆที่นำไปใช้ในการ test อย่างเดียว
ถ้าเป็น local unit test จะใช้ testCompile นำหน้า
ส่วน instrumentation tests จะใช้ androidTestCompile นำหน้า
ref. https://github.com/mockito/mockito/issues/910

ซึ่งโปรเจกตัวอย่างของ google ก็ test ตรงกับที่บอกไว้นะ

มาดูโค้ดในส่วน Testing กันดีกว่า โดยยกตัว SearchViewModel.java มาอธิบาย

ก่อนอื่นประกาศตัวแปร ViewModel, Repository และใส่ @Rule สำหรับประกาศตัวแปร InstantTaskExecutorRule (background thread เอาไว้รันแบบ synchronous สำหรับ Architecture component โดยเฉพาะ)

และทำการ mock-up Repository ที่ @Before

จากนั้นสร้าง Test Case ขึ้นมาที่ @Test ซึ่งมีหลายอันเลย เช่น เรียกขึ้นมาเฉยๆ (empty), มีการ search ชื่อ repository แบบหาเจอ (basic) กับหาไม่เจอ (noObserverNoQuery), search แล้วหาเจอแล้ว มีหลายอันจนต้องโหลดเพิ่ม (swap), refresh หน้าจอทั้งหมด (refresh) กับ search ข้อมูลซํ้า (resetSameQuery)

พอไปดูใน code แล้วก็มี patrick บางอย่างนะ คือใส่ @VisibleForTesting เพื่อสามารถเรียกใช้งานโค้ดส่วนนี้เฉพาะตอนทำการ test เท่านั้น เช่น เพิ่ม function getResults() โดยคืนค่ามาเป็น LiveData ทำให้ viewModel สามารถ observe ได้ ไม่ null แน่นอน

เราว่าโค้ดเขาเขียนดีอยู่นะ เลยทำการ Testing ได้ง่าย
ฝั่งซ้ายเป็นไฟล์ test นามว่า SearchViewModel.java และด้านขวาเป็นไฟล์หลัก นามว่า SearchViewModel.java

3. Repository : <module_name>/src/test/java/

ใช้ JUnit เป็นหลัก และ Mockito ในการ mock data ต่างๆ เช่น service, database, Dao เพื่อสร้าง repository ตัวนึงไว้ที่ @Before และประกาศ InstantTaskExecutorRule ไว้ที่ @Rule เช่นเคย

ตัวโค้ดจะเป็นเช่นนี้ จะมีความคล้าย ViewModel นิดนึง โดยเลือก UserRepository.java มาอธิบาย

ก่อนอื่นประกาศตัวแปร และสร้างกฎกติกาไว้ก่อน โดยสร้าง InstantTaskExecutorRule ขึ้นมาตัวนึง จากนั้นเราก็ mock-up ทุกสิ่งอย่าง

Test case ที่เขาทำไว้ เช่น โหลด user เพื่อมา login (loadUser), เชื่อมต่อ network ภายนอก และสามารถโหลดข้อมูลได้ (goToNetwork), เชื่อมต่อ network ภายนอกไม่ได้ (dontGoToNetwork)

4. Data Source : <module_name>/src/test/java/

เราจะ mock-up server ขึ้นมาตัวนึงเพื่อใช้ test service ที่เราสร้างว่าผลออกมาเป็นอะไร เราจะได้ Object อะไรกลับมา

ดังนั้น เราจะใช้การ Mock-up server ในการ test เนาะ เรียกคนนี้มาช่วย MockWebServer ดังนั้นไปเพิ่ม build.gradle ของ module เสียก่อน

testCompile 'com.squareup.okhttp3:mockwebserver:3.2.0'

จากนั้นเริ่มทำการ mock server และเรียก service จากตัว mock-up server สิ่งที่เราจะ test คือ response code และ response body

ตัว data เราก็ mock up ด้วยเช่นกัน ปกติใส่ไปแบบมั่วๆแบบไม่อิงความจริงใดๆได้เลย และจะใช้ท่าแบบนี้กัน

แต่ในตัวอย่างก็ mock file ขึ้นมาจริงจังเลย

หน้าตา class ที่ใช้ Test GithubService.java

ขั้นแรกสร้าง mock server และ service ใช้ท่า Retrofit ตามปกติเลย และหลังจาก Test เสร็จแล้ว ก็ให้ mock server ของเรา shutdown ไปซะ

ตามปกตินั้น เราจะ test กันแบบนี้

  • ถ้า response code 200, response body ที่ได้ คือ data ก้อนนึง ซึ่งค่าไม่เท่ากับ null
  • ถ้า response code 404 ซึ่งมันก็คือ 404 Not Found หน่ะเนอะ ไม่สามารถรับ data ก้อนนี้กลับมา, response body ที่ได้ คือ null

แต่ด้วยความที่เป็น Android Architecture Component ส่วนใหญ่จะได้ output เป็น LiveData กัน ดังนั้น ใช้ท่าปกติไม่ได้จ้า เลยต้องเปลี่ยนกระบวนท่าบางอย่าง ดังนี้

  • เพื่อให้ได้ object ก้อนนึงที่เสมือนการเรียก service ปกติเราจะใช้การสร้างตัวแปร Response ตัวนึงขึ้นมา แล้วก็ .body ออกมา ซึ่งในที่นี้ทำไม่ได้ เราต้องเอา LiveData เข้าไปใส่ใน Observer ก่อน ถึงจะได้ object ที่ว่าออกมา ซึ่งใน
    โปรเจกเขาทำแบบนี้

ดังนั้นการ compare object ก็สามารถทำท่า assertNotnull ได้ตามเดิม หรือ assertThatตามเขาก็ได้ แต่ดูยุ่งยากกว่าสำหรับเราอ่ะ

assertNotNull(yigit);
assertThat(yigit, notNullValue())

และเราสามารถตรวจสอบแต่ละ attribute ของ object ได้ด้วยนะ เช่น

assertThat(yigit.company, is(“Google”));
  • มี RecordedRequest ไว้ตรวจสอบ request ที่มาจาก mock server ของเรา และสามารถตรวจสอบ path ที่เรียกได้ด้วย
RecordedRequest request = mockWebServer.takeRequest();
assertThat(request.getPath(), is("/users/yigit"));
  • ทำไมไม่มีการ check response code หล่ะ เพราะมันเป็น LiveData ด้วยมั้ง เราพยายามลองแล้ว แต่ไม่มีอันที่ใช้ได้อ่ะ

มาเข้าเรื่องการทำงานทำการเลยแล้วกัน ก่อนอื่นมีการประกาศตัวแปร ตั้งกฏกติกา และสร้าง MockWebServer ที่ @Before

แต่ละ test case มีดังนี้

  • getUser() : ดึง user ขึ้นมา และดูว่า path ของมันถูกไหม ได้ข้อมูล user กลับมาหรือเปล่า ข้อมูลที่ได้ถูกไหม ในที่นี้ตรวจสอบ path avatar ของ user คนนั้น บริษัทที่ทำงาน และบล็อก
  • getRepos() : ดูว่า user คนนั้นๆมี repo อะไรอัพขึ้น github ไปบ้าง อยู่ที่ path อะไร โดยอิตาคนนี้มี repo ขึ้นไปกี่อัน แต่ละอันชื่ออะไรบ้าง ประมาณนี้
  • getContributors() : ดูว่าตาคนนี้นั้น ไปช่วยเขาเขียนโค้ดที่ repo ไหนอีกบ้าง ของใคร
  • search() : ค้นหาบางอย่างใน github อาจจะเป็นชื่อ user หรือ ชื่อ repository ก็ได้ คาดว่า search เจอและได้ข้อมูลกลับมา

เมื่อ run test case ทั้งหมดแล้ว สั่งให้มีการ shutdown mock-up server ของเราซะ

สรุป แต่ละ test case ในนี้ ก็ทดสอบการเรียก service ในแต่ละตัวนั่นแหละ โดย mock server และ mock ไฟล์ json ที่เป็น output ด้วย เพื่อ check object ที่ได้ว่าตรงกันไหม

5. Model ส่วนหลักที่ทำ Unit Testing คือ DAO (Data Access Object) นั่นเอง โดย location ของไฟล์อยู่ที่<module_name>/src/androidTest/java/

อันนี้ถือเป็นของใหม่เลยสำหรับการ test ร่วมกันกับ SQL โดยใช้ AndroidJUnit4 เป็นหลัก และ ใช้ SQLiteOpenHelper เป็นผู้ช่วย

ก่อนอื่นสร้าง class ที่เรียก database สำหรับการ test แยกไว้ ชื่อ DbTest

การเขียน Test ปกติที่ผ่านมา จะเขียน 1 test case ต่อ 1 function ใน class นั้นๆ แต่สำหรับไฟล์ Dao นั้น จะไม่เหมือนปกติ คือ เขียน 1 test case ต่อ 1 event เช่น ใส่ข้อมูลเข้า database แล้วอ่าน, ใส่เข้าแล้วลบออก, เปลี่ยนแปลงข้อมูลบางอย่างในแต่ละ field ซึ่งใช้หลายๆ function ใน event นั้นๆ

ตรง createUser(), createRepo(), createRepos() และ createContributor() นั้น จะถูกสร้างใน TestUtil ซึ่งหน้าตาของ function นี้จะเป็นดังนี้

แต่ละ test case นั้น เราจะมีการสร้าง mock-up ของ repo (model class ที่เป็น POJO นั่นแหละ) ขึ้นมาก่อนเสมอ โดยแต่ละ test case จะมีการทำงานที่ไม่เหมือนกัน ดังนี้

  • insertAndRead() : ใส่ข้อมูลลง database แล้วนำมาอ่าน โดยใช้ท่าการดึง object เหมือนตอน test data source
  • insertContributorsWithoutRepo() : มีการ mock-up date ของ contributor ด้วย ตรวจสอบว่าเราสามารถ insert data ของ contributor ได้ไหม
  • insertContributors() : mock-up data ของ contributor 2 ตัว, insert repo และ contributors ทั้ง 2 ตัว และ ตรวจสอบดูว่า สามารถเรียก contributor ที่เพิ่งใส่ไปได้หรือไม่
  • createIfNotExists_exists() : ตรวจสอบว่า เมื่อใส่ repo ลง database แล้ว not exist จะไม่เป็นจริง
  • createIfNotExists_doesNotExist() : ไม่ใส่ repo แล้ว not exist เป็นจริง
  • insertContributorsThenUpdateRepo() : ตรวจสอบการ update repo และ contributor บน database

เก็บตกนิดนึง เราแปะเว็บในการทดลองเขียนคำสั่ง SQL เผื่อช่วยได้นะ

https://www.w3schools.com/sql/trysql.asp?filename=trysql_select_where


สุดท้ายนี้ เราคิดว่าเราได้เขียนเรื่อง Android Architecture Components ไปหมดแล้วแหละมั้ง ในระหว่างทำ Research ครั้งนี้ ก็ได้ทำอะไรใหม่ๆ ที่เป็นเรื่องใหม่ๆ ที่เพิ่งมาจาก Google I/O 2017 เพื่อการประยุกต์ใช้จริง

ถ้าในแอปฟังใจมีการเปลี่ยนโครงสร้างโค้ดเป็นแบบนี้จริง เราจะมาบอกเล่ากันอีกทีนะ ว่าเป็นอย่างไร ในระหว่างที่มีการแก้ไข structure อาจจะมีการเสียเนื้อ เสียเลือด ไปบ้าง โปรดเป็นกำลังใจแก้ทีม product ของฟังใจต่อไป สวัสดีค่ะ :)

Tags

Minseo Chayabanjonglerd

I am a full-time Android Developer and part-time contributor with developer community and web3 world, who believe people have hard skills and soft skills to up-skill to da moon.