มาทำ Unit Test สำหรับ Android Architecture Components กันเถอะ
หลังจากที่เราได้ศึกษาเรื่อง 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 คืออะไร
(ตอนแรกว่าจะรวม ไปๆมาๆเริ่มเยอะไปแล้ว เลยเขียนบล็อกแยกอีกอันดีกว่า)
สำหรับคนที่หลงมาอ่านแล้วงงๆว่า Android Architecture Components คืออะไร อ่านสองบล็อกย้อนหลังได้เลยจ้า
มาเริ่มทำ Testing ของ Architecture Components แต่ละตัวกันดีกว่า
ตามปกติแล้วนั้น ส่วนของการ Testing จะซ่อนตัวกันแบบนี้
ส่วนใหญ่หลังจากที่เราสร้างโปรเจกเสร็จ
แล้วสำหรับ Architecture Components หล่ะ?
ตาม guideline ของ google และจากที่ได้อ่านมา แบ่งแต่ละส่วนประมาณนี้
- 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 ได้เลยนะ ถ้าทำความเข้าใจโค้ดดีๆ
โค้ดชุดนี้ทำอะไร? เป็นแอป 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 ที่มีส่วนผสมของมินต์ มะนาว นํ้าตาล และแอลกอฮอลเล็กน้อย มีขิงหน่อยๆ
เดี๋ยวนะ มันใช่หรอ!
Mockito ชื่อเหมือนเครื่องดื่มชนิดหนึ่งนั่นแหละ และก็เป็นชื่อของ library ที่ใช้ในการทำ Unit Test บน Android โดย stub object ที่เราไม่ได้ test เช่น ในตอนนี้เราจะ test หน้า Activity เราใช้ mockito mock ViewModel ขึ้นมา หรือ object อื่นๆที่เอามาแสดงผล ซึ่งในที่นี้ test การกดแบบต่างๆ แล้วต้องแสดงผลตามที่เราต้องการ
คำสั่งที่ใช้กัน ก็มีประมาณในรูปนี่แหละ
- สร้าง mock-up object โดยใช้คำสั่ง
mock()
และไส้ในคือ object ที่เราจะทำการ mock-up ขึ้นมา เช่น ViewModel - เราใส่พฤติกรรมการทำงานของ object ที่ถูกทำงาน ไว้ใน
when()
spy()
ใช้กับ object จริง ที่ไม่ผ่านการ mock-up ซึ่งในที่นี้ก็ไม่ได้ใช้นะverify()
ใช้กับ function ที่มีการรับค่า argument เข้ามา เพื่ออ่านค่าที่ได้จาก function นั้นๆ แต่ไม่ได้คืนค่าอะไรออกมาแบบนั้นนะ แค่บอกว่าทำได้ไม่ได้แค่นั้น
มาดูโค้ดกันดีกว่า เราเลือกไฟล์ที่ test SearchFragment.java มายกตัวอย่าง
ถามว่าแต่ละอัน test อะไร ดูคลิปประกอบดีกว่าเนอะ ง่ายกว่า 555 มันจะรันไปทีละอัน จากบนไปล่างนั่นแหละ
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 ได้ง่าย
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 ของฟังใจต่อไป สวัสดีค่ะ :)