มาเขียนเทสกัน กับงาน Android Codelabs Together ตอน Testing Basics

Event Jun 29, 2020

ฝั่ง Firebase เขาก็มีกันไปแล้ว เป็น Codelab เขียนเว็บ แล้วฝั่ง Android จะยอมน้อยหน้ากันได้อย่างไรจริงม่ะ

งานจะเริ่มในวันอาทิตย์ที่ 21 มิถุนายน เวลาสองทุ่มถึงสี่ทุ่ม มีความเริ่มดึกๆหล่ะ แต่เข้าใจได้ว่าทำไมเป็นเวลานี้ 5555555555 โดยพบกับคนคุ้นเคย คือ คุณเอก Android GDE และน้องเบน นั่นเองงงงงงง

จริงๆในเพจ Google Developer Group Thailand ก็ได้เฉลยแล้วว่า Codelab ที่เราจะมาทำไปพร้อมๆกันในงาน Android Codelabs Together นั่นก็คือ Testing Basics นั่นเอง

ช่องทางการรับชมจะอยู่ที่นี่จ้า ชมย้อนหลังได้เลย

Android Codelabs Together - Testing Basics

เจอกันตอน 2 ทุ่มนะครับ เตรียมตัวให้พร้อมแล้วมาทำ Codelab ไปพร้อมๆกันเถอะ

Posted by Google Developer Group Thailand on Sunday, June 21, 2020
https://www.facebook.com/GDGThailand/videos/2754685334759907/

หรือดูย้อนหลังใน YouTube ก็ย่อมได้

https://www.youtube.com/watch?v=7jfMX6Ap_f0

ก่อนรับชมอย่าลืมอัพเดต Android Studio เป็น version ล่าสุดก่อนนะ ซึ่งก็น่าจะเป็น version 4.0 แหละ

ถึงเวลาแล้ว มาปักรอดูกัน เอ่อออออออ ดูไลฟ์ยังไงนะ ฮืออออออ

และในที่สุดก็หาไลฟ์เจอในเพจ GDG Thailand จ้า

ช่วงแรกจะมีพี่โอ๋มาเปิดงาน พร้อมข่าวอัพเดตเล็กๆน้อยๆจ้า

  • event ของ Google จะ focus ที่ online event เป็นหลัก ทำให้ทุกคนได้เข้าพร้อมกันทั่วโลกแบบไม่ต้องเดินทางไปไหน เช่น Android 11
  • event ถัดไปก็จะเป็น web.dev LIVE นั่นเอง สำหรับสายเว็บโดยเฉพาะ เป็นงาน Global เนอะ เราทิ้งวาปไว้ที่นี่จ้า เข้าไปในเว็บเราจะเห็น Agenda ของแต่ละวัน โดยงานจะจัดวันที่ 30 มิถุนายน - 2 กรกฏาคม จ้า
web.dev LIVE
Bringing web developers together, from home
https://web.dev/live/
  • วันที่ 14 กรกฏาคม - 8 สิงหาคม จะมีงาน Google Cloud Next OnAir จ้า เปิดตัว speaker แล้วแต่ยังไม่ได้เปิด agenda เนอะ
  • เห็นพี่ตี๋มา comment ว่ามีงาน Firebase Live นะ เริ่มวันที่ 23 มิถุนายนนี้แล้ว จัดเป็น week ๆ เข้าไปส่องตารางได้ด้านล่างจ้า
ประกาศ Google Cloud Next ’20: OnAir | Google Cloud Blog
เริ่มตั้งแต่วันที่ 14 กรกฎาคม OnAir ครั้งถัดไปจะมีเนื้อหาสดใหม่ทุกสัปดาห์ ประกอบด้วยเซสชันต่างๆ มากกว่า 200 เซสชัน โดยมีทั้งประเด็นสำคัญจากผู้มีชื่อเสียงในอุตสาหกรรมไปจนถึงโอกาสเรียนรู้เนื้อหาขั้นสูงกับนักพัฒนาซอฟต์แวร์แนวหน้าของ Google
https://cloud.google.com/blog/topics/google-cloud-next/announcing-google-cloud-next20-onair-th
Firebase Live 2020
Firebase Live is a web series for app developers consisting of talks, tips, and technical tutorials aimed at increasing their productivity, knowledge, and collaboration. Hosted by the familiar faces of the Firebase team, each week we’ll release a new video highlighting the latest best practices and…
https://firebaseonair.withgoogle.com/events/firebase-live20

มาเริ่มทำ Codelab~ กันเลย

ทำไมเลือกอันนี้หล่ะ Automate testing—the basics เพราะอยากให้ Android Developer เขียน Test กันมากขึ้นนั่นเอง ซึ่งทางเราได้เดาถูกแล้วแหละ ในที่น้ีจะเป็นการทำ Unit Test นั่นเอง

google.dev
Learn the basics of testing your Android Kotlin apps. In this codelab you'll learn to run tests, write basic tests, work with AndroidX Test, as well as test ViewModel and LiveData.
https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#0

แต่ทำเสร็จวันนี้ไม่ได้ badge นะ เพราะมันอยู่ใน Advanced Android in Kotlin นั่นเอง ในนั้นมีทั้งหมด 6 บทด้วยกัน แต่ทางเราไปเจอใน Add advanced features to Android apps in Kotlin ซึ่งเป็นชุดสำหรับเรียนรู้การเป็น Android Kotlin Developer นั่นเอง

Welcome

มาเล่าเรื่องกันสักหน่อยเนอะ การเขียนเทสนั้น แก้ปัญหาโค้ดที่เคยเขียนแล้วพออกลับมาดูแล้วก็งงๆ ว่าโค้ดนี้ทำงานอะไรนะ แล้วก็ทำให้เราทำ CI/CD ได้ง่ายด้วยนะ และการเขียนโค้ดด้วยเทสนั้น ทำให้ทำของใหม่ๆได้เร็วกว่า เพราะการทำ manual test กับให้ tester ไปเทส อาจจะมีบางเคสที่หลุดๆไปบ้าง

ด้วย test case ที่เราทำไว้ ทำให้เราสามารถตรวจสอบได้ว่าโค้ดที่เราเขียนนั้น ทำงานไม่แย่ไปจากเดิม

ดังนั้นใน codelab นี้จะมีการทำ Unit Test กับ ViewModel และ LiveData ด้วย เพราะว่าโปรเจกตัวอย่างนี้นั้น เป็น MVVM เนอะ และตัวโปรเจกใน codelab เราก็สามารถเข้าไปศึกษาดูได้ว่าเขาเขียนกันยังไงนะ

แล้ว TDD คืออะไรกันหล่ะ? TDD ย่อมาจาก Test-Driven-Development จะเป็นการเขียนเทสก่อนเขียนโค้ดใน feature นั้นจริงๆ เช่น เรามี feature A เราจะมาเขียนเทสกันก่อนว่า feature นี้ควรจะมี test case อะไรบ้าง และนำมาเขียนโค้ดให้ test passed และถ้ามีการ refactor code เราจะสามารถตรวจสอบการทำงานของโค้ดที่เรา refactor ไปด้วย test case เหล่านี้ว่าที่เพิ่งแก้ไปยังทำงานถูกต้องเหมือนเดิมหรือไม่

App overview

โปรเจคตัวอย่างนี้ เป็นแอพ Todo ที่สร้าง task ได้ ภายในใส่ title และ detail ลงไปในนั้น อันไหนทำเสร็จแล้วก็ติ๊กครับ และมีความจริงจังถึงขึ้นมี filter ด้วยนะ ว่าอันไหนทำเสร็จยัง อันไหนยังทำไม่เสร็จ และลบ task ที่เราต้องการ หรือลบหมดเลยงี้

ยืมรูปมาจาก https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#1

Getting Started

เป็นขั้นตอนที่ยากที่สุดเลย555555 หยอกๆจ้า ให้ download zip หรือ clone git ลงมา

googlecodelabs/android-testing
Android Testing Codelab. Contribute to googlecodelabs/android-testing development by creating an account on GitHub.
https://github.com/googlecodelabs/android-testing.git

จากนั้นเปิดใน Android Studio รอมัน sync gradle เสร็จ ซึ่งกินเวลาตามเครื่องที่เราใช้ ของเราก็ประมาณ 5 นาทีได้ ก่อนหน้านี้เลือก config แอพไม่ได้ต้องแก้อีก ก็ราวๆ 10 นาทีแหน่ะ

Task: Familiarizing yourself with the code

มาทำความรู้จักของโปรเจกนี้กันว่าทำอะไรได้บ้างอ่ะเราอ่ะ

package ของ feature จะมี 4 อัน คือ

  • addedittask เพิ่มหรือแก้ task
  • statistics แสดง stat ของ task
  • taskdetail แสดงรายละเอียดของ task นั้นๆ
  • task แสดง task ทั้งหมดที่มี

แล้วก็มี util พวก extension ต่างๆ ซึ่งใครๆก็ทำกันอยู่แล้ว เช่น extension ของ snackbar อ่ะเนอะ และ data จะมีพวก database, network, repository ในนั้น

structure ตัวนี้ จะถูกใช้ใน codelab ตัวอื่นๆด้วย เช่น โปรเจกใน udacity, sunflower

android/sunflower
A gardening app illustrating Android development best practices with Android Jetpack. - android/sunflower

และเป็น Single-page Activity ด้วย (ทางเราจะพยายามเขียนบล็อกเกี่ยวกับเรื่องนี้ต่อไป เนื่องจากแอพอ่านบล็อกเราก็เป็นแนวทางนี้เช่นกัน) เพราะว่าเขาอยากให้ focus กับ core feature ของแอพที่เราทำกันนั่นเอง

มาดูโครงสร้างภายในกัน

ยืมรูปมาจาก https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#3
  • แต่ละ feature นั้น จะประกอบไปด้วยส่วน View ที่อาจจะเป็น Fragment และ ViewModel
  • มีเจ้า Repository เป็นคนดึงข้อมูลและเก็บข้อมูลให้เรา use case ที่เราพบเห็นได้ประจำคือ ใน Facebook เรากด like โพสนึง ตอนเลื่อนกลับไปดูก็ต้องเห็นว่าเรากด like โพสนี้ไปแล้วเนอะ ซึ่งการทำงานเบื้องหลังคือ ถ้าเรากด like โพสนั้นแล้ว แต่พอดีว่า internat สัญญาณไม่ดี เลยส่ง data ไปยัง server ไม่ได้ จึงเก็บลง local storage ในที่นี้คือ Room ไปก่อน ถ้า internet กลับมาดีแล้ว จึงจะส่ง data ไปยัง server ผ่าน Network หรืออีกเคสนึงคือ หิวข้าวมาก กดสั่งอาหารในแอพ food delivery เราต้องเห็นอาหารที่เราเลือกในตะกร้า ถ้าหายไปคงไม่ค่อยอยากจะใส่เพิ่มใหม่กัน
  • Network ในที่นี้เป็นการเขียน data sourse จำลองให้เขียนเทสได้ง่ายขึ้น ปกติก็คือส่วนที่เรียก API นั่นแหละ
  • data binding ทำให้เราสามารถทำ two-way binding ได้ง่ายขึ้น เช่นปกติเราจะ set text เองผ่านโค้ด แต่อันนี้เราสามารถ update ค่าเข้าไปใน view ได้เลย
เพิ่มเติมจากเจ้าของบล็อก ใน C# ที่เป็น MVVM ก็จะมี data binding เป็นปกติกันอยู่แล้วนะ
[Tutorial] การเขียน Datagrid เบื้องต้น แบบ WPF/MVVM
หลังจากคราวก่อนที่สอนเขียน MVVM เบื้องต้นไปแล้วในวันนี้จะมาทบทวนกันอีกครั้ง พร้อมกับการเขียน datagrid แบบ MVVM ด้วยภาษา C#ซึ่งเราได้ค้นหาข้อมูลเพิ่มเติมในการเขียน และนำมาสรุปเพื่อให้ผู้อ่านทำความเข้าใจมากขึ้น

ในที่นี้จะตัด data binding และ navigation component (เส้นลากต่างๆ เช่น action นึกง่ายๆจะเหมือน Storyboard ของ iOS อ่ะ) ออกไปก่อน

Task: Running tests

เกริ่นกันมายาวนาน มาทำเทสกันเถอะ 555555555555

ในที่นี้ก็จะมี test 2 ประเภท ก็คือ

  • local test (test) การทำ Unit Test ปกติจะทำลงช่องนี้กัน รันโดย jvm บนคอมของเรา จะเน้นเทสโค้ด logic จ๋าๆ
  • instrumental test (androidTest) เป็นการเทสโดยใช้ resource ของ device มาสร้าง environment เพื่อทำการรันเทส จะเน้นเทสกับสิ่งที่เป็น Android จ๋าๆ

ทั้งสองส่วนนี้จะไม่ฝังไปกับ app ด้วย และมี source set ที่เราสามารถไปแยก build varient เช่น staging, production ยิง server คนละตัวกัน โดยให้ gradle build ให้โดยไม่ต้องสลับ endpoint ดังนั้นเราจะต้องใส่ให้ถูกต้องด้วยนะ

การรันเทส กดที่ปุ่ม play เขียวๆ มีแบบ test function เดียว กับ test ทุก function ใน class นั้นๆ

จิ้มตรงชื่อ function คือเทส function เดียว จิ้มตรงชื่อ class คือเทสทุก function ใน class

ถ้ามีหลายไฟล์ กดที่ folder แล้วคลิกขวา แล้วเขาจะรันเทสให้เราเนอะ

ถ้าเราเคยเลือกกด run อะไรตรงไหน ทั้ง build หรือ test ก็สามารถกดไปดูได้ และถ้าเยอะเกินไปก็เข้าไปลบได้

ลองทำให้ test failed หน่อยซิ๊ เพิ่มไป 1 บรรทัด แน่นอน fail แบบไม่ต้องสืบ เราจะได้ AssertionError มาเป็นที่ระทึก

เมื่อเรามีเจ้า test case เยอะๆ แล้วมี test failed น้อยๆ เราสามารถประหยัดเวลาชีวิตได้โดยกดตรง Rerun Failed Tests ที่มีเครื่องหมายตกใจสีแดงๆ

มาลองรัน Instrumental test กันเถอะ ตัวอย่างเราจะทำการ check package name ว่ามันตรงกันไหมนะ แน่นอนมันจะนานหน่อย เพราะมีการ sync gradle เหมือนไปรันแอพในเครื่องจริงๆเลย

และนี่คือผลที่ได้จ้า

Task: Writing your first test

ได้เวลาเขียนเทสกันเสียที555 โดยการจำลอง data ใน class ที่เราจะเทสนั่นเอง

มาสร้างเทสกัน class ที่เราทำกระทำการเทสกันนั่นคือ StatisticsUtils โดยจะเทส function getActiveAndCompletedStats สร้างไฟล์เทสขึ้นมาใหม่โดยเลือก Generate -> Test... หรือ key ลัดก็คือ Command + N ที่ชื่อ function ก็ได้นะเออ

จากนั้นจะมี Dialog ขึ้นมา เลือก Testing library เป็น JUnit4 ส่วน Class name สามารถเปลี่ยนได้นะ แล้วยังไม่ต้องติ๊กอะไรนะ เรียบร้อยแล้วกด OK

อันนี้ต้องระวัง เพราะเลือกผิด ชีวิตเปลี่ยนทันที ตั้งสติกันก่อน เราทำ local test เนอะ ดังนั้นเลือก folder ที่เป็น test นะ

ถ้าทำถูกต้องหน้าตาจะเป็นแบบนี้

มาเขียนเทสกันได้ล้าววว ในที่นี้เราอยากเทสว่า ถ้าไม่มี task ไหนที่ทำเสร็จ และมี 1 task ที่ยัง active อยู่ ดังนั้น active tasks เป็น 100% และ complete tasks เป็น 0% โดย function นี้จะชื่อว่า getActiveAndCompletedStats_noCompleted_returnsHundredZero

และใส่ annotation @Test ลงไป เพื่อให้รู้ว่า อันนี้เป็น function ที่เราจะใช้ทำเทสกันนะ

จากนั้นทำการเพิ่มสิ่งต่างๆ ซึ่งสุดท้ายจะเป็นดังนี้ และก็ควรจะ test passed อะนะ

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}

การเอาของมาเปรียบเทียบกัน มักจะใช้แบบนี้ assertEquals(expect, actual) เพื่อให้กลับไปอ่าน test case แล้วเข้าใจว่าเราต้องการเทสค่าไหนอยู่งี้ จึงมักเอา expect ไว้หน้า actual กัน

Hamcrest คือ ให้ code ของ test case ของเรา สามารถอ่านได้ง่ายขึ้น เหมือนอ่านประโยคในภาษาอังกฤษ หรือใช้ truth.dev ก็ได้

ก่อนอื่นใส่ library ของ Hamcrest ก่อน และการใส่ source set testImplementation ก็คือส่วนนี้จะ build เฉพาะตอนเราเทสเท่านั้น

dependencies {
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

โดยตัว source set ของ local test จะเป็น testImplementation และ instrumental test จะเป็น androidTestImplementation นะ

มาใช้ Hamcrest กันเถอะ~ เปลี่ยนจากของเดิมกันดีกว่า

import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`

// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
  • จาก assertEquals ไปเป็น assertThat เนอะ โดยการใส่ parameter นั้นมาตาม concept เดิมเนอะ
  • ส่วน `is` จริงๆแล้ว is เป็นคำเฉพาะใน Kotlin อ่ะเนอะ เลยต้องมี ` ครอบคำไว้ มีเพื่อให้อ่านง่ายขึ้นว่าใช่อันนี้ไหม
  • การกดเลือก import เลือก import org.hamcrest.Matchers.is นะ

ทริคเด็ดๆที่ใน codelab ตอนนี้ได้บอกเราไว้

  • structure ในการเขียนเทส ก็คือ given, when, then โดยแต่ละส่วนนั้นมีหน้าที่ดังนี้ given คือ เตรียม object ที่ใช้ในการเทส, when คือ ต้องการ execute คำสั่งที่ต้องการเทสตรงไหน, และ then คือ assert value เพื่อไม่ให้เทสของเราซับซ้อนหรือสับสน
  • Test Names มีรูปแบบการตั้งชื่อ คือ subjectUnderTest_actionOrInput_resultState มาอธิบายแต่ละส่วนกัน subjectUnderTest คือเราจะเทส function ของเทสชื่ออะไร, actionOrInput มี action หรือ input อะไรของ Test Case นี้ และ returnState ควรได้อะไร เพื่อให้เรากลับไปดูได้ว่า test case ไหน fail
  • การคิด Test Case นั้น ควรดู  Test Case ที่ tester คิดประกอบกันไปด้วย เพื่อให้เราสามารถเขียนเทสครอบคลุมในส่วนนั้นด้วย แต่บางทีอาจจะเป็นแค่ Test Senerio ก็ได้นะ
มีคำถามจากทางบ้านที่คอมเมนต์ในไลฟ์ น่าสนใจมากๆ
Q : Instrumental Test สามารถทำใน CircleCI หรือ Bitrise ได้ไหมครับ (คำถามนี้เราเข้าใจว่าเอาไปทำใน CI ได้ไหมนะ)
A ตอบโดยน้องเบน : ได้ครับผม สามารถต่อ CI เราเข้ากับ Remote Device ผ่านพวก Firebase Test Lab ได้ครับ

Task: Writing more tests

เขียนเทสให้ cover มากขึ้นสิ ในตอนนี้จะเริ่มทำ TDD กันหล่ะ

ยืมรูปจาก https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#6

ว่าแต่ตัว function ทำอะไรบ้างนะ? แล้วมันควรจะมี Test Case อะไรบ้างหล่ะ?

  • getActiveAndCompletedStats_noCompleted_returnsHundredZero()
  • getActiveAndCompletedStats_noActive_returnsZeroHundred()
  • getActiveAndCompletedStats_both_returnsFortySixty()
  • getActiveAndCompletedStats_error_returnsZeros()
  • getActiveAndCompletedStats_empty_returnsZeros()

ตามหลัก TDD มันจะต้อง test failed ก่อนรอบนึง

ทางเราเดาว่าน่าจะ bug ตรงหารด้วย 0 เพราะที่ tasks นั้นสามารถรับค่าที่เป็น null เข้ามาได้ด้วย ทำให้เราไม่สามารถหา size ของมันได้ หรือถ้ามันมี size = 0 ทำให้ test case มีอันที่ test failed 1 อัน และ code crash 1 อัน

และก็เป็นจริงอย่างที่เราคิดไว้

หลังจากนั้นแก้บัคสิ ให้ test ที่ failed นั้นเป็น passed ซะ โดยทำการ check ว่า list มัน null ไหม และมันไม่ empty หรือเปล่า ตอนแรกโค้ดจะไม่สวย ก็ค่อยๆ test ไป refactor ไป สุดท้ายจะได้ดังนี้ เทสผ่านแน่นอน

Task: Setting up a ViewModel Test with AndroidX Test

ความจริงคือมันสี่ทุ่มแล้ว เริ่มไม่ไหวหล่ะ -_-zZ

ViewModel นั้น เป็นศูนย์ควบคุมโควิท เอ้ยย logic ที่เกิดขึ้นในแอพ และเป็นตัวกลางระหว่าง Activity/Fragment (ส่วน View นั่นแหละ) กับ Repository

ดังนั้นในบทนี้จะเริ่มมาทำเทสกับเจ้า ViewModel กันแล้วจ้าาา โดยจะทำการเทสที่ TasksViewModel โดยในนี้ทำการ handle logic ในแต่ละ feature ทำให้ตัวไฟล์นี้มันดูเยอะๆนิดนึงเนอะ

ยืมอีกล้าวมาจาก https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#7

ไปดูที่ addNewTask() ดูเหมือนไม่ได้ถูกเรียกใช้ที่ไหน แต่จริงๆถูกเรียกใช้ที่ data binding

ส่วน Event นั้นเป็น class ที่ถูกสร้างไว้ในโปรเจกนี้ ออกแบบมาเพื่อใช้ความสามารถของ LiveData แต่มีปัญหานึงคือไม่ได้สร้างจาก LiveData แต่ใช้ร่วมกับ LiveData คือ LiveData ใช้ส่งข้อมูลในลักษณะคล้ายๆกับการ stream data มีปลายทางเป็น observe data ที่อยู่ใน LiveData เมื่อข้อมูลเกิดการเปลี่ยนแปลง ก็จะมีอัพเดตไปให้ทันที สิ่งที่ observe ก็จะเปลี่ยนทันที

แต่ถ้าอยากโยน Event เข้าไปหล่ะ เป็น Event ที่มี data ชุดนึง และไม่อยากให้เป็น streaming data ที่อัพเดต data ตลอดเวลา และอยากโยนไปครั้งเดียวจบ เลยอยากให้โยนผ่าน LiveData เหมือนเดิม เพราะข้อดีของ LiveData คือมีเรื่อง lifecycle awareness เช่น ดึงข้อมูลบางอย่างจาก service แล้ว user พับจอลงไป แม้จะโหลดข้อมูลมาเสร็จแล้ว จะยังไม่อัพเดตจน user เข้ามา active ในแอพ

getContentIfNotHandled() ทำหน้าที่คือ เมื่ออัพเดตข้อมูลเสร็จแล้วให้เคลียร์ค่าทิ้งหมดเลย เพื่อให้ได้ข้อมูลเพียงครั้งเดียว peekContent() ใช้สำหรับ component บางตัวอยาก observe ตัวเดียวกัน แต่ไม่ได้อยากดึงข้อมูลออกไป

ตอนใช้งานจริง ต้องมี EventObserver ซึ่ง Event ตัวนี้นั้นไปเขียนครอบจาก Observer อีกทีนึง เพื่อให้มันดึง content ขึ้นมาโดยอัตโนมัติ

ตัวอย่างการเอาไปใช้ เรียก ViewModel ขึ้นมา และ observe สิ่งที่เรียกว่า editTaskEvent โดยตัวมันเองจะเป็น LiveData<Event<Unit>> การที่เอา Event ไปใส่ไว้ใน LiveData ก็เพื่อไว้จำแนกข้อมูลประเภท Event

พอเรียกใช้ ViewModel ตามมาด้วย LiveData และหลังจาก observe เสร็จก็จะทำการใส่ lifecycle ลงไป ตามมาด้วย EventObserver เพื่อดึง content ข้างในให้ทันทีโดยอัตโนมัติโดยไม่ต้องพิมพ์เอง

//TaskDetailViewModel.kt
private val _editTaskEvent = MutableLiveData<Event<Unit>>()
val editTaskEvent: LiveData<Event<Unit>> = _editTaskEvent

//TaskDetailFragment.kt
viewModel.editTaskEvent.observe(this, EventObserver {
    val action = TaskDetailFragmentDirections
        .actionTaskDetailFragmentToAddEditTaskFragment(
            args.taskId,
            resources.getString(R.string.edit_task)
        )
    findNavController().navigate(action)
})

ทำไมถึงต้องทำ EventObserver ให้เราหล่ะ? เพราะปกติการเรียกใช้จะเป็น Observer เนอะ เมื่อได้ content ข้างในแล้ว จะต้องทำการ it.getContentIfNotHandled() ทุกครั้งนั่นเอง

viewModel.editTaskEvent.observe(this, Observer {
    it.getContentIfNotHandled()
})

จากนั้นมาเริ่มเขียนเทสเจ้า ViewModel แล้วนะ ให้สร้างเป็น Local Test นะ

แล้วทำไมเขียนเทส ViewModel ใน local test หล่ะ? เพราะว่าการทำ Instumental Test นั้นใช้เวลารันนาน และกว่าจะเทสครบก็นาน เพราะต้อง connect device เพื่อดึง resource มาใช้ เลยให้ใส่ใน local เพื่อความไว

การเรียก ViewModel ขึ้นมา ต้องการ Application นะ เอ๊ะจะยังไงต่อดีนะ มันเป็น class ใน Android นี่นา

ทางเขาก็เลยมี AndroidX Test libraries ช่วยแก้ปัญหาในจุดนี้ โดยการจำลอง class ที่อยู่ใน Android framework ให้เราได้ โดยเราไม่ต้องทำ Instrumental Test ในกรณีนี้อีกต่อไป และเพิ่ม Robolectric Testing library ช่วย run Android framework ใน local test ได้

ไปเพิ่ม dependency กันเถอะ build หนึ่งที

// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"

จากนั้นใส่ runner @RunWith(AndroidJUnit4::class) ด้วยตรงบน class โดย runner นั้นจะมากับการทำ Instrumental Test เพราะเรารันบนเครื่องอื่น และต้องมีคบควบคุมว่าจะไป run ให้ใคร ด้วยวิธีไหน เช่นตัวนี้ จะทำให้เรา run Android Test ที่อยู่ข้างใน junit local test ได้ ถ้าไม่ใส่จะ run บน jvm บนเครื่องเราเป็น default

เดี๋ยวมันจะหาเนื้อแกะให้เรา อ๊ะไม่ช่ายยย ยืมรูปจาก https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#7
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

และเราก็ใส่ ApplicationProvider เพื่อทำให้เรา get context มาได้

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()

และทำการอัญเชิญ tasksViewModel.addNewTask() มา จากนั้นทำการ run สิ่งที่พบก็คือ โหลดของอื่นๆมาให้เราตอน run test เนอะ

สิ่งที่น่ามหัศจรรย์ก็คือ warning นั่นเองงง แต่ก็ไม่น่าขึ้น เพราะโปรเจกเป็น Android SDK 28 อ่ะ ถ้าเราใช้เป็น Android SDK 29 ต้องไปใช้ Java 9 นั่นเอง

แก้โดยไปใส่สิ่งในข้างใน Android scope อีกที เพื่อให้เข้าถึง Android Resource ในตอนทำ Unit Testing นั่นเอง

testOptions.unitTests {
    includeAndroidResources = true
}

Task: Writing Assertions for LiveData

กลับมาเขียน assertion ของ LiveData ต่อกันเตอะ (แต่ด้วยความง่วงเลยไม่ได้จด เลยจดใหม่เลย)

ปัญหาของการทำเทสคือ อย่าเขียนเทสแบบ asynchronous เพราะว่าเทสจะรันแบบ synchronous นะจ๊ะ เลยจะเกิดปัญหาคือ test passed โดยที่ยังไม่ทันจะไปถึง assertion เลย

และ Unit Test ก็คือเทสใน scope ภายในนั้น ไม่ควรทำให้มันช้า โดยการไปเรียก dependency อื่นๆมางี้ ตัวอย่างที่เรานึกได้ไวๆก็คือ เทสว่าถ้า response code ได้ 200 ควรได้อะไรกลับมา ดังนั้นจึง mock service ไว้

มีสิ่งที่เรียกว่า InstantTaskExecutorRule มาคอยช่วยเหลือเรา หน้าที่ก็ตรงตัว เป็น Task Executor สำเร็จรูป ทำให้บางอย่างที่ run อยู่ใน background สามารถ execute ได้เลยทันที ใช้เฉพาะตอนเทสเท่านั้น! ตัวนี้จะมา solve ปัญหาว่าเราจะเทส LiveData อย่างไรให้เป็น synchronous

ดังนั้นตอนนี้ import เข้ามาเลยยยย

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"

เมื่อ import เสร็จประกาศ test rule ให้กับ junit ที่เราจะทำการเทส โดยใช้ annotation @get:Rule เพื่อประกาศตัวแปรของ test class นั้นๆเลย มีชีวิตตั้งแต่เกิดจนตายไป การประกาศ rule นี้ทำให้เรา trigger ของที่อยู่ใน ViewModel ได้จริงๆหล่ะ

@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()

และมาเขียน assertion กัน มี try-finally ด้วย ความยากคือ import ให้ถูกอันด้วยน้าาา ทางเราแปะอันที่ถูกต้องให้แล้วนะ

จริงๆมีทริคอยู่ว่าอย่าเลือก import ที่มีตัว core นะ

ผลคือควรได้อะไรบางอย่างที่ไม่เป็น null เนอะ แน่นอนมันจะผ่าน

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

โค้ดตอนนี้ยังไม่สวยนะ observe บ่อยๆใจมันไม่ไหวเนอะ

ดังนั้นก้อปเจ้า LiveDataTestUtil เข้ามาด้วยพร้อม import มี getOrAwaitValue() ข้างในจะ trigger observeForever ให้เอง และมี handle หน่วงเวลาให้เราด้วย

การนำไปใช้ก็ลบโค้ดยุ่งๆทิ้งไป แล้วใช้เจ้า getOrAwaitValue() และไม่มีโอกาสส่ง null ถ้าของไม่มีจะรอจนกว่าจะมีของแล้วค่อยทำต่อ เห็นม่ะ โค้ด่านง่ายขึ้นแล้ว อ่ะเทสใหม่มันควรจะผ่านเนอะ

ข้อควรระวัง Observe LiveData ให้ถูกตัวด้วยนะ

Task: Writing multiple ViewModel tests

เขียนเทสให้มั่นใจว่าชัวร์!

ไปเขียน setFilterAllTasks_tasksAddViewVisible() มาเพิ่ม เพื่อ test การ filter all tasks เนอะ โดย type ต่างๆจะเป็น enum ที่ชื่อว่า TasksFilterType นั่นเอง

เริ่มทำการ test โดยใช้ ViewModel เดิมที่ชื่อว่า TasksViewModel และเราทำการเทส filter all tasks tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

และทำการ assert ของ เป็นการจำลองการเลือก all tasks

assertThat(
    tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), 
    `is`(true)
)

ผลคือเทสผ่านหล่ะ

และถ้ามี function ของ test case เยอะขึ้นเรื่อยๆหล่ะ เราต้องสร้าง ViewModel ใหม่ทุกครั้งเลยนี่นา

ดังนั้นเราจะใช้ annatation ที่ชื่อว่า @Before เพื่อเตรียมคำสั่งที่เราใช้ในทุก test case โดยที่เราไม่ต้องเสียเวลามา copy and paste ทุกครั้ง

จึงนำเจ้า TasksViewModel ไปประกาศตัวแปรไว้ด้านบนเป็น lateinit แล้วไปกำหนดค่าที่ function อันนึง ชื่อว่า ซีตุ๊ป เอ้ยย setupViewModel เพื่อ init ค่าของเจ้า ViewModel นี้ และบน function นี้ใส่ @Before ลงไป

// Subject under test
private lateinit var tasksViewModel: TasksViewModel

@Before
fun setupViewModel() {
    tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}

แต่ปกติใช้ชื่อ setup() กันมากกว่า

ทั้งหมดจะเป็นดังนี้ เอา ViewModel ที่ประกาศใน test case ออกไป

และต้องเทสผ่านนะจ๊ะ

ข้อควรระวัง มันมากับความสงสัยที่ว่า ทำไมไม่ assign ค่าให้กับเจ้า ViewModel ไปเลยหล่ะ จะมานั่ง lateinit มาใส่ค่าในซีตุ๊ปตะไม? เพราะว่าการทำแบบนั้นเราจะได้ ViewModel ตัวเดิม สำหรับทุกเคส ดังนั้นเราควรสร้าง given ใหม่ทุกครั้ง ไม่งั้นจะมีความกระทบกันได้ ทำให้ test case confilct กัน เพราะบางทีเจ้า ViewModel โดนเขียนอะไรบางอย่างลงไปแล้ว มันจะเพี้ยนเนอะ

และ codelab ที่เราลงมือเทสก็ได้หมดลงแล้วววว

Summary

  • เรารันเทสผ่าน Android Studio ได้อย่างไร
  • ความแตกต่างระหว่าง local test (test) และ instrumentation tests (androidTest) คือมี source set ที่ต่างกัน
  • การเขียนเทสโดยการใช้ JUnit และ Hamcrest.
  • การเขียนเทสกับ ViewModel กับ AndroidX Test Library.

และเขาก็ได้ทิ้ง Resource ต่างๆให้ด้วย อ่ะไปจิ้มดูกัน

google.dev
Learn the basics of testing your Android Kotlin apps. In this codelab you&#39;ll learn to run tests, write basic tests, work with AndroidX Test, as well as test ViewModel and LiveData.
https://google.dev/codelabs/advanced-android-kotlin-training-testing-basics?authuser=1#12

ทำไมจึงรันเทสบน command line ก็เพราะใช้ใน CI นั่นแหละ ใครจะเปิดเครื่องจิ้มๆแบบ auto อ่ะเนอะ เขาจะ generate resource มาให้ โดนไม่ต้องเปิด Android Studio แหละ

การเขียนเทสบน ViewModel สำคัญที่สุด เพราะ ViewModel เป็นคนถือ logic ต่างๆในแอพของเราเนอะ

จบแล้วววววว~~~~~ ทางเราขออนุญาติตัด Q & A ออกไปอีกตามเคย อยากให้ไปฟังเองมากกว่าอ่ะ อิอิ

ถ้าอยากอ่านบล็อกเกี่ยวกับ Unit Test เพิ่มเติม จิ้มอ่านได้ด้านล่างจ้า

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

ส่วนข้อติติงน้านนนน มีเรื่องเดียวจริงๆ คือเรื่องเวลา ดูจะเป็นปัญหาด้วยแหละ เพราะดันจัดคืนวันอาทิตย์ และวันจันทร์ก็ต้องไปทำงานกัน แล้วลากยาวไป 3 ชั่วโมง เราเลยหนีไปนอนตอนสี่ทุ่มเลย ถ้าแบบจัดสักบ่ายโมง บ่ายสอง จะลากไปห้าหกโมงเย็นก็ยังพอได้อยู่นะ

แต่ข้อดีแน่ๆคือ มีคนสอนเขียนเทสและทำตาม codelab นี่แหละ ทำให้เราเข้าใจมันมากขึ้นเนอะๆ

รอว่าคราวหน้าจะจัดอีกเมื่อไหร่เนอะ เดายากอยู่เช่นกัน

สุดท้าย บล็อกเขียนเสร็จแล้ว ฝากร้านได้

อย่าลืมกด like กด share บทความกันด้วยนะคะ :)

Posted by MikkiPastel on Sunday, 10 December 2017

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.