Codelab นี้เป็นส่วนหนึ่งของหลักสูตร Android ขั้นสูงใน Kotlin คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้ หากเรียนผ่าน Codelab ตามลําดับ แต่ไม่บังคับ Codelab ของหลักสูตรทั้งหมดจะแสดงอยู่ในหน้า Landing Page ขั้นสูงของ Android ใน Kotlin Codelab
บทนำ
Codelab การทดสอบที่ 2 นี้เป็นการทดสอบแบบ 2 เท่า คือกรณีที่ควรใช้ใน Android และวิธีติดตั้งโดยใช้การแทรกทรัพยากร Dependency, รูปแบบตัวระบุตําแหน่งบริการ และไลบรารี ในการเรียนรู้นี้ คุณจะได้เรียนรู้วิธีเขียนดังนี้
- การทดสอบหน่วยที่เก็บ
- ส่วนย่อยและการทดสอบการผสานรวมโมเดล
- การทดสอบการนําทางที่เป็น Fragment
สิ่งที่ควรทราบอยู่แล้ว
คุณควรทําความคุ้นเคยกับสิ่งต่อไปนี้
- ภาษาโปรแกรม Kotlin
- แนวคิดการทดสอบที่ครอบคลุมใน Codelab แรก ได้แก่ การเขียนและการทดสอบหน่วยใน Android โดยใช้ JUnit, Hamcrest, AndroidX Testing, Robolectric รวมถึงการทดสอบ LiveData
- ไลบรารีหลักของ Android Jetpack:
ViewModel
,LiveData
และคอมโพเนนต์การนําทาง - สถาปัตยกรรมแอปพลิเคชันตามรูปแบบจากคู่มือสถาปัตยกรรมแอปและ Codelab พื้นฐานของ Android
- ข้อมูลเบื้องต้นเกี่ยวกับ Coroutine ใน Android
สิ่งที่คุณจะได้เรียนรู้
- วิธีวางแผนกลยุทธ์การทดสอบ
- วิธีสร้างและการใช้การทดสอบแบบคู่ คือปลอมและจําลอง
- วิธีใช้การแทรกทรัพยากร Dependency ด้วยตนเองใน Android สําหรับการทดสอบหน่วยและการผสานรวม
- วิธีใช้รูปแบบตัวระบุบริการ
- วิธีทดสอบที่เก็บ ส่วนย่อย ดูโมเดล และคอมโพเนนต์การนําทาง
คุณจะใช้ไลบรารีและแนวคิดโค้ดต่อไปนี้
สิ่งที่คุณจะทํา
- เขียนการทดสอบหน่วยสําหรับที่เก็บโดยใช้การทดสอบแบบ 2 เท่าและการแทรกการขึ้นต่อกัน
- เขียนการทดสอบหน่วยโฆษณาสําหรับโมเดลข้อมูลพร็อพเพอร์ตี้โดยใช้การทดสอบแบบ 2 เท่าและแทรกการขึ้นต่อกัน
- เขียนการทดสอบการผสานรวมสําหรับส่วนย่อยและโมเดลข้อมูลพร็อพเพอร์ตี้โดยใช้เฟรมเวิร์กการทดสอบ UI ของ Espresso
- เขียนการทดสอบการนําทางโดยใช้ Mockito และ Espresso
ใน Codelab ชุดนี้ คุณจะต้องทํางานร่วมกับแอป TO-DO Notes ซึ่งช่วยให้คุณเขียนงานให้เสร็จและแสดงในรายการได้ จากนั้นทําเครื่องหมายว่าเสร็จสมบูรณ์ กรอง หรือลบ
แอปนี้เขียนขึ้นใน Kotlin มีหน้าจอหลายหน้าจอ ใช้คอมโพเนนต์ Jetpack และปฏิบัติตามสถาปัตยกรรมจากคําแนะนําเกี่ยวกับสถาปัตยกรรมแอป การเรียนรู้วิธีทดสอบแอปนี้จะทําให้คุณทดสอบแอปที่ใช้ไลบรารีและสถาปัตยกรรมเดียวกันได้
ดาวน์โหลดโค้ด
ดาวน์โหลดโค้ดเพื่อเริ่มต้นใช้งาน
หรือโคลนที่เก็บ GitHub สําหรับโค้ดได้ดังนี้
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
ใช้เวลาสักครู่เพื่อทําความคุ้นเคยกับโค้ดโดยทําตามคําแนะนําด้านล่าง
ขั้นตอนที่ 1: เรียกใช้แอปตัวอย่าง
เมื่อดาวน์โหลดแอป TO-DO แล้ว ให้เปิดแอปใน Android Studio และเรียกใช้ ควรรวบรวม สํารวจแอปโดยทําตามขั้นตอนต่อไปนี้
- สร้างงานใหม่ด้วยปุ่มการทํางานแบบลอยและบวก ป้อนชื่อเรื่องก่อน จากนั้นป้อนข้อมูลเพิ่มเติมเกี่ยวกับงาน บันทึกโดยใช้ FAB เครื่องหมายถูกสีเขียว
- ในรายการงาน ให้คลิกชื่องานที่เพิ่งทําเสร็จ แล้วดูหน้าจอรายละเอียดของงานนั้นเพื่อดูคําอธิบายที่เหลือ
- ในรายการหรือในหน้าจอรายละเอียด ให้เลือกช่องทําเครื่องหมายของงานนั้นเพื่อตั้งสถานะเป็นเสร็จสมบูรณ์
- กลับไปที่หน้าจองาน เปิดเมนูตัวกรอง และกรองงานตามสถานะใช้งานอยู่และเสร็จสมบูรณ์
- เปิดลิ้นชักการนําทาง แล้วคลิกสถิติ
- กลับไปที่หน้าจอภาพรวม และเลือกล้างงานที่เสร็จสมบูรณ์เพื่อลบงานทั้งหมดที่มีสถานะเสร็จสมบูรณ์จากเมนูงาน
ขั้นตอนที่ 2: สํารวจโค้ดของแอปตัวอย่าง
แอป "สิ่งที่ต้องทํา" นั้นมาจากตัวอย่างการทดสอบและสถาปัตยกรรมของ Architecture Blueprints ที่ได้รับความนิยม (โดยใช้เวอร์ชันสถาปัตยกรรมเชิงรับ) แอปจะทําตามสถาปัตยกรรมจากคู่มือสถาปัตยกรรมแอป โดยใช้ View Models กับ Fragments, ที่เก็บ และ Room หากคุณคุ้นเคยกับตัวอย่างด้านล่าง แอปนี้มีสถาปัตยกรรมที่คล้ายกัน
- ห้องที่มี View Codelab
- Codelab การฝึกอบรม Kotlin Fundamentals ขั้นพื้นฐาน
- Codelab การฝึกอบรม Android ขั้นสูง
- ตัวอย่าง Android Sunflower
- การพัฒนาแอป Android ด้วยหลักสูตรการฝึกอบรม Kotlin Udacity
คุณจําเป็นต้องทําความเข้าใจสถาปัตยกรรมทั่วไปของแอปมากกว่าการทําความเข้าใจตรรกะในเลเยอร์ใดชั้นหนึ่ง
ด้านล่างนี้เป็นข้อมูลสรุปแพ็กเกจที่คุณจะพบ
แพ็กเกจ: | |
| เพิ่มหรือแก้ไขหน้าจองาน: โค้ดเลเยอร์ UI สําหรับการเพิ่มหรือแก้ไขงาน |
| ชั้นข้อมูล: ใช้กับชั้นข้อมูลของงาน ซึ่งประกอบด้วยฐานข้อมูล เครือข่าย และโค้ดที่เก็บ |
| หน้าจอสถิติ: รหัสเลเยอร์ UI สําหรับหน้าจอสถิติ |
| หน้าจอรายละเอียดงาน: รหัสเลเยอร์ UI สําหรับงานเดียว |
| หน้าจองาน: รหัสเลเยอร์ UI สําหรับรายการงานทั้งหมด |
| ชั้นเรียนยูทิลิตี้:ชั้นเรียนที่แชร์และใช้ในส่วนต่างๆ ของแอป เช่น สําหรับเลย์เอาต์การรีเฟรชแบบปัดที่ใช้หลายหน้าจอ |
ชั้นข้อมูล (.data)
แอปนี้มีเลเยอร์เครือข่ายจําลองในแพ็กเกจระยะไกลและเลเยอร์ฐานข้อมูลในแพ็กเกจภายใน เพื่อความสะดวก โปรเจ็กต์นี้จะจําลองชั้นเครือข่ายด้วย HashMap
ที่มีความล่าช้า ไม่ใช่การส่งคําขอเครือข่ายจริง
พิกัดหรือสื่อกลางของ DefaultTasksRepository
ระหว่างเลเยอร์เครือข่ายและเลเยอร์ฐานข้อมูลคือสิ่งที่ส่งคืนข้อมูลไปยังเลเยอร์ UI
เลเยอร์ของ UI ( .addedittask, .statistics, .taskdetail, .tasks)
แพ็กเกจเลเยอร์ UI แต่ละรายการมีส่วนย่อยและโมเดลข้อมูลพร็อพเพอร์ตี้ ตลอดจนคลาสอื่นๆ ที่จําเป็นสําหรับ UI (เช่น อะแดปเตอร์สําหรับรายการงาน) TaskActivity
คือกิจกรรมที่มีส่วนย่อยทั้งหมด
การไปยังส่วนต่างๆ
การนําทางสําหรับแอปจะควบคุมโดยคอมโพเนนต์การนําทาง ดังที่ระบุไว้ในไฟล์ nav_graph.xml
ระบบจะทริกเกอร์การนําทางในโมเดลข้อมูลพร็อพเพอร์ตี้โดยใช้คลาส Event
นอกจากนี้ โมเดลข้อมูลพร็อพเพอร์ตี้ยังกําหนดอาร์กิวเมนต์ที่จะส่งผ่านด้วย Fragment จะสังเกต Event
และทําการนําทางจริงระหว่างหน้าจอ
ใน Codelab นี้ คุณจะได้เรียนรู้วิธีทดสอบที่เก็บ ดูโมเดล และส่วนย่อยโดยใช้การทดสอบแบบคู่และการแทรกทรัพยากร Dependency ก่อนที่จะเจาะลึกถึงรายละเอียดเหล่านั้น คุณควรทําความเข้าใจเหตุผลที่จะเป็นแนวทาง และคุณจะเขียนการทดสอบเหล่านี้อย่างไร
ส่วนนี้จะครอบคลุมแนวทางปฏิบัติแนะนําโดยทั่วไปของการทดสอบซึ่งเกี่ยวข้องกับ Android
พีระมิดทดสอบ
เมื่อพิจารณากลยุทธ์การทดสอบ การทดสอบมีอยู่ 3 ส่วนที่เกี่ยวข้องดังนี้
- ขอบเขต - การทดสอบจะแตะที่โค้ดเท่าไร โดยสามารถทําการทดสอบได้วิธีเดียวทั้งแอปพลิเคชันหรือที่อื่นๆ
- ความเร็ว - ทําการทดสอบเร็วเพียงใด ความเร็วทดสอบอาจแตกต่างกันไปตั้งแต่หลายมิลลิวินาทีไปจนถึงหลายนาที
- ความแม่นยํา - การทดสอบ "real-world" เป็นอย่างไร ตัวอย่างเช่น หากโค้ดส่วนหนึ่งที่คุณทดสอบต้องการส่งคําขอเครือข่าย โค้ดทดสอบจะสร้างคําขอเครือข่ายนี้จริง หรือปลอมผลลัพธ์ขึ้นจริง หากการทดสอบพูดคุยกับเครือข่ายจริงๆ จะหมายถึงความแม่นยําสูงขึ้น ข้อดีของการทดสอบก็คือการทดสอบอาจใช้เวลานานกว่านั้น และอาจส่งผลให้เกิดข้อผิดพลาดหากเครือข่ายล่มหรืออาจมีค่าใช้จ่ายสูง
ข้อดีของทั้ง 2 ฝ่ายจึงมีความแตกต่างกัน เช่น ความเร็วและความแม่นยําจึงหมายถึงการได้รับข้อได้เปรียบ โดยยิ่งทดสอบได้เร็วขึ้น ความแม่นยําก็จะยิ่งต่ําลง และในทางกลับกันด้วย วิธีทั่วไปในการแบ่งการทดสอบอัตโนมัติออกเป็น 3 หมวดหมู่ต่อไปนี้
- การทดสอบหน่วยการเรียนรู้ - การทดสอบที่เน้นเนื้อหาเป็นหลักซึ่งทํางานในชั้นเรียนเดียว ซึ่งมักจะเป็นวิธีเดียวในชั้นเรียนนั้น หากการทดสอบหน่วยไม่สําเร็จ คุณจะทราบได้อย่างไรว่าปัญหาอยู่ตรงไหนในโค้ด การทดสอบมีความแม่นยําต่ํามากในชีวิตจริง แอปจึงมากกว่าการดําเนินการตามวิธีการหรือคลาสเพียงอย่างเดียว โค้ดเหล่านี้ทํางานได้รวดเร็วทุกครั้งที่คุณเปลี่ยนโค้ด ซึ่งมักจะเป็นการทดสอบภายในระบบ (ในชุดแหล่งที่มา
test
) ตัวอย่าง: การทดสอบวิธีการเดียวในโมเดลข้อมูลพร็อพเพอร์ตี้และที่เก็บ - การทดสอบการผสานรวม - การทดสอบการโต้ตอบของหลายๆ ชั้นเรียน เพื่อให้แน่ใจว่าลักษณะการทํางานจะทํางานตามที่คาดไว้เมื่อใช้ร่วมกัน วิธีหนึ่งในการจัดโครงสร้างการทดสอบการผสานรวมคือการทดสอบฟีเจอร์เดียว เช่น ความสามารถในการบันทึกงาน โปรแกรมจะทดสอบขอบเขตโค้ดที่ใหญ่กว่าการทดสอบหน่วยโฆษณา แต่ยังคงได้รับการเพิ่มประสิทธิภาพเพื่อให้ทํางานได้อย่างรวดเร็วเมื่อเทียบกับความแม่นยําทั้งหมด โดยอาจทดสอบในเครื่องหรือเพื่อทดสอบการใช้งานก็ได้ ขึ้นอยู่กับสถานการณ์ ตัวอย่าง: การทดสอบฟังก์ชันทั้งหมดของคู่ส่วนย่อยและมุมมองรูปแบบเดียว
- การทดสอบแบบจุดต่อจุด (E2e) - ทดสอบชุดฟีเจอร์ที่ทํางานร่วมกัน และทดสอบข้อมูลส่วนใหญ่ของแอป จําลองการใช้จริงอย่างใกล้ชิด และมักจะช้า มีความแม่นยําสูงสุดและบอกคุณได้ว่าแอปพลิเคชันของคุณจะทํางานโดยรวมได้อย่างแท้จริง การทดสอบขนาดใหญ่นี้จะทําการทดสอบแบบรวมหลายรายการ (ในชุดแหล่งที่มา
androidTest
)
ตัวอย่าง: การเริ่มต้นทั้งแอปและทดสอบฟีเจอร์ 2-3 รายการร่วมกัน
สัดส่วนที่แนะนําของการทดสอบเหล่านี้มักจะแสดงด้วยพีระมิด โดยการทดสอบส่วนใหญ่จะเป็นการทดสอบแบบหน่วย
สถาปัตยกรรมและการทดสอบ
ความสามารถในการทดสอบแอปในทุกระดับของพีระมิดทดสอบจะเชื่อมโยงกับสถาปัตยกรรมของแอป ตัวอย่างเช่น แอปพลิเคชันที่มีสถาปัตยกรรมต่ํามากอาจนําตรรกะทั้งหมดมาใช้ภายในวิธีการเดียว คุณอาจเขียนการทดสอบแบบจุดต่อจุดได้เพราะการทดสอบเหล่านี้มักจะเป็นการทดสอบเนื้อหาส่วนใหญ่ของแอป แต่เป็นการทดสอบการเขียนหน่วยหรือการผสานรวมใช่ไหม โค้ดทั้งหมดในที่เดียวช่วยให้คุณทดสอบโค้ดที่เกี่ยวข้องกับหน่วยหรือฟีเจอร์เดียวได้ยาก
วิธีที่ดีกว่าคือแจกแจงตรรกะของแอปพลิเคชันออกเป็นหลายๆ คลาสและหลายคลาส ซึ่งทําให้แต่ละทดสอบแยกกันได้ สถาปัตยกรรมคือวิธีแบ่งและจัดระเบียบโค้ด ซึ่งช่วยให้การทดสอบหน่วยและการผสานรวมง่ายขึ้น แอปสิ่งที่ต้องทําที่จะทดสอบเป็นไปตามสถาปัตยกรรมที่เฉพาะเจาะจง
ในบทเรียนนี้ คุณจะได้ทดสอบวิธีทดสอบสถาปัตยกรรมต่างๆ ข้างต้นแบบแยกกัน
- ก่อนอื่น คุณจะต้องทดสอบหน่วยโฆษณาที่เก็บ
- จากนั้นจะใช้การทดสอบเป็น 2 เท่าในโมเดลข้อมูลพร็อพเพอร์ตี้ ซึ่งจําเป็นสําหรับการทดสอบหน่วยโฆษณาและการทดสอบการผสานรวม
- ถัดไป คุณจะได้เรียนรู้วิธีเขียนการทดสอบการผสานรวมสําหรับส่วนย่อยและโมเดลข้อมูลพร็อพเพอร์ตี้
- ขั้นสุดท้าย คุณจะได้ดูวิธีเขียนการทดสอบการผสานรวมที่มีคอมโพเนนต์การนําทาง
เราจะกล่าวถึงการทดสอบตั้งแต่ต้นจนจบในบทเรียนถัดไป
เมื่อเขียนการทดสอบหน่วยสําหรับส่วนหนึ่งในชั้นเรียน (วิธีการหรือชุดเล็กๆ ของวิธีการ) เป้าหมายของคุณคือเพื่อทดสอบรหัสในชั้นเรียนนั้นเท่านั้น
การทดสอบเฉพาะโค้ดในชั้นเรียนหนึ่งหรือบางคลาสอาจเป็นเรื่องยาก ยกตัวอย่างเช่น เปิดคลาส data.source.DefaultTaskRepository
ในชุดแหล่งที่มา main
นี่คือที่เก็บของแอปและเป็นชั้นที่คุณจะเขียนการทดสอบหน่วยให้ถัดไป
เป้าหมายของคุณคือการทดสอบเฉพาะโค้ดในชั้นเรียนนั้น แต่ DefaultTaskRepository
ขึ้นอยู่กับคลาสอื่นๆ เช่น LocalTaskDataSource
และ RemoteTaskDataSource
จึงจะทํางานได้ กล่าวอีกอย่างคือ LocalTaskDataSource
และ RemoteTaskDataSource
เป็นการขึ้นต่อกันของ DefaultTaskRepository
ดังนั้น ทุกเมธอดใน DefaultTaskRepository
จะเรียกใช้เมธอดในชั้นเรียนแหล่งข้อมูล ซึ่งจะนําไปสู่วิธีการโทรในชั้นเรียนอื่น เพื่อบันทึกข้อมูลลงในฐานข้อมูลหรือสื่อสารกับเครือข่าย
ตัวอย่างเช่น ลองดูวิธีการนี้ใน DefaultTasksRepo
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}
getTasks
เป็นหนึ่งในสายที่เรียกใช้มากที่สุดสําหรับโควต้า วิธีนี้รวมถึงการอ่านจากฐานข้อมูล SQLite และการเรียกใช้เครือข่าย (การเรียก updateTasksFromRemoteDataSource
) ซึ่งรวมถึงโค้ดที่เก็บมากกว่าเพียงโค้ดที่เก็บ
เหตุผลที่เจาะจงมากขึ้นสาเหตุที่การทดสอบที่เก็บนั้นเป็นเรื่องยากมีดังนี้
- คุณต้องคิดเกี่ยวกับการสร้างและจัดการฐานข้อมูลเพื่อทําการทดสอบที่เรียบง่ายที่สุดสําหรับที่เก็บนี้ การทําเช่นนี้จะนําไปสู่คําถามต่างๆ เช่น " และควรจะเป็นการทดสอบหรือไม่ใส่รายการทดสอบในท้องถิ่น และควรใช้ AndroidX Test เพื่อสร้างสภาพแวดล้อมจําลองของ Android หรือไม่
- โค้ดบางส่วน เช่น โค้ดเครือข่าย อาจต้องใช้เวลาทํางานนานๆ หรือบางครั้งอาจทํางานล้มเหลว ทําให้เกิดการทดสอบที่ไม่น่าเชื่อถือเป็นเวลานาน
- การทดสอบอาจสูญเสียความสามารถในการวิเคราะห์โค้ดที่ผิดพลาดของการทดสอบ การทดสอบอาจเริ่มทดสอบโค้ดที่ไม่ใช่ที่เก็บ ตัวอย่างเช่น ตัวอย่างเช่น การทดสอบหน่วย "repository" อาจไม่สําเร็จเนื่องจากปัญหาในโค้ดบางส่วน เช่น โค้ดฐานข้อมูล
คู่ทดสอบ
วิธีแก้ปัญหาคือเมื่อคุณทดสอบที่เก็บ อย่าใช้เครือข่ายหรือโค้ดฐานข้อมูลจริง แต่ให้ใช้การทดสอบซ้ํา 2 ครั้งแทน การทดสอบแบบคู่คือเวอร์ชันของชั้นเรียนที่สร้างขึ้นมาเพื่อการทดสอบโดยเฉพาะ ซึ่งมีไว้เพื่อแทนที่ชั้นเรียนเวอร์ชันจริงในการทดสอบ ซึ่งคล้ายกับการแสดงผาดโผนของนักแสดงที่มีความเชี่ยวชาญในการแสดงผาดโผน และมาแทนที่นักแสดงตัวจริงในการกระทําที่เป็นอันตราย
การทดสอบประเภทสองมีดังนี้
ปลอม | การทดสอบแบบคู่ที่มี "การทํางาน&โควต้า; การใช้งานในชั้นเรียน แต่มีการใช้ในลักษณะที่ได้ผลดีสําหรับการทดสอบแต่ไม่เหมาะสําหรับการผลิต |
จําลอง | การทดสอบอีก 1 ครั้งที่ติดตามว่ามีการใช้เมธอดใด จากนั้นจะผ่านการทดสอบหรือไม่ผ่านก็ได้ ขึ้นอยู่กับว่าระบบเรียกใช้เมธอดอย่างถูกต้องหรือไม่ |
Stub | การทดสอบแบบ 2 เท่าที่ไม่มีตรรกะและแสดงผลเฉพาะโปรแกรมที่คุณส่งคืนเท่านั้น เช่น |
จําลอง | มีการทดสอบประมาณ 2 อย่างที่ผ่านไปแล้วแต่ไม่ได้ใช้ เช่น หากคุณเพียงต้องการระบุเป็นพารามิเตอร์ หากคุณมี |
สปาย | การทดสอบอีก 1 ครั้งจะเก็บการติดตามข้อมูลเพิ่มเติมไว้ด้วย เช่น หากคุณสร้าง |
ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบคู่ได้ที่การทดสอบบนโถสุขภัณฑ์: รู้เท่าทันการทดสอบ
การทดสอบแบบคู่ที่ใช้ใน Android มากที่สุด ได้แก่ Fakes และ Mocks
ในงานนี้ คุณจะได้สร้างการทดสอบ FakeDataSource
คู่กับการทดสอบหน่วย DefaultTasksRepository
ที่แยกออกจากแหล่งข้อมูลจริง
ขั้นตอนที่ 1: สร้างคลาส FakeDataSource
ในขั้นตอนนี้ คุณจะต้องสร้างชั้นเรียนชื่อ FakeDataSouce
ซึ่งจะเป็นการทดสอบคู่ของ LocalDataSource
และ RemoteDataSource
- ในซอร์สโค้ดการทดสอบ ให้คลิกขวาที่ New -> Package
- สร้างแพ็กเกจข้อมูลที่มีแพ็กเกจแหล่งที่มาภายใน
- สร้างชั้นเรียนใหม่ชื่อ
FakeDataSource
ในแพ็กเกจ data/source
ขั้นตอนที่ 2: ใช้อินเทอร์เฟซ TasksDataSource
หากต้องการใช้ FakeDataSource
ของชั้นเรียนใหม่เป็นการทดสอบซ้ําได้ คุณต้องแทนที่แหล่งข้อมูลอื่นๆ ได้ แหล่งข้อมูลเหล่านั้นคือ TasksLocalDataSource
และ TasksRemoteDataSource
- โปรดสังเกตว่าทั้ง 2 รายการนี้ใช้อินเทอร์เฟซ
TasksDataSource
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- ทําให้
FakeDataSource
ใช้งานTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio จะร้องเรียนว่าคุณไม่ได้ติดตั้งใช้งานเมธอดที่จําเป็นสําหรับ TasksDataSource
- ใช้เมนูการแก้ไขด่วนและเลือกใช้สมาชิก
- เลือกวิธีทั้งหมดและกดตกลง
ขั้นตอนที่ 3: ใช้เมธอด getTasks ใน FakeDataSource
FakeDataSource
คือการทดสอบบางประเภทที่เฉพาะเจาะจงซึ่งเรียกว่าปลอม ปลอมคือการทดสอบซ้ําที่มีการใช้ & & quot;การทํางาน & quot; ในคลาส แต่มีการใช้ในแนวทางง่ายๆ ที่เหมาะสําหรับการทดสอบ แต่ไม่เหมาะสําหรับการผลิต "Working" การใช้งานหมายความว่าชั้นเรียนจะให้ผลลัพธ์จริงตามอินพุตที่กําหนดไว้
ตัวอย่างเช่น แหล่งข้อมูลปลอมจะไม่เชื่อมต่อเครือข่ายหรือบันทึกข้อมูลใดๆ ลงในฐานข้อมูล แต่จะใช้เพียงรายการในหน่วยความจําเท่านั้น วิธีนี้จะทํางานตามที่คุณคาดหวัง & ไม่ได้ ในวิธีรับหรือบันทึกงานจะส่งคืนผลลัพธ์ที่คาดหวัง แต่คุณไม่สามารถใช้การติดตั้งใช้งานนี้ในระบบที่ใช้งานจริงได้ เนื่องจากจะไม่มีการบันทึกไปยังเซิร์ฟเวอร์หรือฐานข้อมูล
FakeDataSource
- ช่วยให้คุณทดสอบโค้ดใน
DefaultTasksRepository
ได้โดยไม่ต้องอาศัยฐานข้อมูลหรือเครือข่ายจริง - ให้การดําเนินการ "real-พอ) สําหรับการทดสอบ
- เปลี่ยนเครื่องมือสร้าง
FakeDataSource
เพื่อสร้างvar
ที่ชื่อว่าtasks
ซึ่งเป็นMutableList<Task>?
ที่มีค่าเริ่มต้นเป็นรายการที่สามารถเปลี่ยนแปลงได้
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
นี่คือรายการงานที่ "fakes" เป็นฐานข้อมูลหรือการตอบสนองของเซิร์ฟเวอร์ ในตอนนี้ คุณต้องทดสอบเมธอด repository'sgetTasks
ซึ่งเรียกว่าแหล่งข้อมูล getTasks
, deleteAllTasks
และ saveTask
เขียนเมธอดเหล่านี้ในรูปแบบปลอม
- เขียน
getTasks
: หากtasks
เป็นnull
ไม่ได้ ให้แสดงผลSuccess
หากtasks
คือnull
ให้ส่งคืนผลลัพธ์Error
- เขียน
deleteAllTasks
: ล้างรายการงานที่เปลี่ยนแปลงได้ - เขียน
saveTask
: เพิ่มงานลงในรายการ
วิธีการเหล่านี้ซึ่งใช้สําหรับ FakeDataSource
จะดูเหมือนโค้ดด้านล่าง
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}
นี่คือคําสั่งนําเข้าหากจําเป็น
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
ซึ่งคล้ายกับลักษณะการทํางานของแหล่งข้อมูลในพื้นที่และระยะไกล
ในขั้นตอนนี้ คุณจะต้องใช้เทคนิคที่เรียกว่าการฉีดทรัพยากร Dependency ด้วยตนเองเพื่อให้ใช้การทดสอบปลอมที่เพิ่งสร้างขึ้น
ปัญหาหลักคือคุณมี FakeDataSource
แต่ไม่มีความชัดเจนว่าคุณจะใช้การทดสอบอย่างไร คุณจําเป็นต้องเปลี่ยน TasksRemoteDataSource
และ TasksLocalDataSource
แต่ใช้ได้เฉพาะในการทดสอบเท่านั้น ทั้ง TasksRemoteDataSource
และ TasksLocalDataSource
มีการขึ้นต่อกันของ DefaultTasksRepository
ซึ่งหมายความว่า DefaultTasksRepositories
จําเป็นต้องมีหรือ &ขึ้นอยู่กับการอ้างอิง&ในคลาสเหล่านี้เพื่อเรียกใช้
ในขณะนี้ ระบบจะสร้างทรัพยากร Dependency ในเมธอด init
ของ DefaultTasksRepository
DefaultTasksRepository.kt
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}
เนื่องจากคุณกําลังสร้างและมอบหมาย taskLocalDataSource
และ tasksRemoteDataSource
ภายใน DefaultTasksRepository
ระบบจึงฮาร์ดโค้ดเป็นหลัก คุณจะสลับการทดสอบไม่ได้
สิ่งที่คุณต้องทําแทนผลคือให้แหล่งข้อมูลเหล่านี้แก่ชั้นเรียนแทนการฮาร์ดโค้ด เราจะเรียกว่าการแทรกทรัพยากร Dependency วิธีการมอบทรัพยากร Dependency มีอยู่หลายวิธี จึงมีผลกับการฉีดทรัพยากร Dependency ประเภทต่างๆ
การแทรกตัวสร้างเครื่องมือสร้างช่วยให้คุณสลับการทดสอบเป็น 2 เท่าได้โดยการส่งผ่านเข้าไปในตัวสร้าง
ไม่ฉีดยา | การฉีดยา |
ขั้นตอนที่ 1: ใช้การแทรกตัวสร้างเครื่องมือสร้างใน DefaultTasksRepository
- เปลี่ยนเครื่องมือสร้าง
DefaultTaskRepository
' จากการลงชื่อเข้าใช้Application
เพื่อยอมรับทั้งแหล่งข้อมูลและผู้เสนอราคา Cortine (ซึ่งคุณจะต้องสลับการทดสอบด้วย (ซึ่งอธิบายรายละเอียดเพิ่มเติมในส่วนบทเรียนที่ 3 เกี่ยวกับ Coroutine))
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
- เนื่องจากคุณนําทรัพยากร Dependency เข้ามาแล้ว จึงนําเมธอด
init
ออก คุณจึงไม่จําเป็นต้องสร้างทรัพยากร Dependency อีกต่อไป - และลบตัวแปรอินสแตนซ์เก่าด้วย คุณกําหนดคําจํากัดความเองในตัวสร้าง:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- สุดท้าย ให้อัปเดตเมธอด
getRepository
เพื่อใช้เครื่องมือสร้างใหม่
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
คุณกําลังใช้การแทรกทรัพยากร Dependency ของตัวสร้าง
ขั้นตอนที่ 2: ใช้ FakeDataSource ในการทดสอบ
เมื่อโค้ดใช้การแทรกการขึ้นต่อกันของเครื่องมือสร้างแล้ว คุณสามารถใช้แหล่งข้อมูลปลอมเพื่อทดสอบ DefaultTasksRepository
ได้
- คลิกขวาที่ชื่อชั้นเรียน
DefaultTasksRepository
แล้วเลือกสร้าง จากนั้นเลือกทดสอบ - ทําตามข้อความที่ปรากฏเพื่อสร้าง
DefaultTasksRepositoryTest
ในชุดแหล่งที่มา test - ที่ด้านบนสุดของตัวแปร
DefaultTasksRepositoryTest
ใหม่ ให้เพิ่มตัวแปรสมาชิกด้านล่างเพื่อแสดงถึงข้อมูลในแหล่งข้อมูลปลอม
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
- สร้างตัวแปร 3 รายการ และตัวแปรสมาชิก
FakeDataSource
รายการ (ตัวแปร 1 รายการสําหรับแหล่งข้อมูลแต่ละรายการ) สําหรับที่เก็บ) และตัวแปรสําหรับDefaultTasksRepository
ที่จะทดสอบ
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
ดูวิธีตั้งค่าและเริ่มต้น DefaultTasksRepository
ที่ทดสอบได้ DefaultTasksRepository
นี้จะใช้การทดสอบ FakeDataSource
- สร้างเมธอดชื่อ
createRepository
และทําคําอธิบายประกอบด้วย@Before
- สร้างอินสแตนซ์แหล่งข้อมูลปลอมโดยใช้รายการ
remoteTasks
และlocalTasks
- สร้างอินสแตนซ์
tasksRepository
โดยใช้แหล่งข้อมูลปลอม 2 แหล่งที่คุณเพิ่งสร้างและDispatchers.Unconfined
วิธีการสุดท้ายควรมีลักษณะเหมือนกับโค้ดด้านล่าง
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
ขั้นตอนที่ 3: เขียนการทดสอบ getTasks() ของ DefaultTasksRepository
ได้เวลาเขียนบททดสอบภาษาDefaultTasksRepository
แล้ว!
- เขียนการทดสอบสําหรับเมธอด
getTasks
ของที่เก็บ ตรวจสอบว่าเมื่อคุณเรียกใช้getTasks
กับtrue
(ซึ่งหมายความว่าควรโหลดซ้ําจากแหล่งข้อมูลระยะไกล) เพื่อดึงข้อมูลจากแหล่งข้อมูลระยะไกล (ซึ่งตรงข้ามกับแหล่งข้อมูลในเครื่อง)
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
คุณจะได้รับข้อผิดพลาดเมื่อโทรหา getTasks:
ขั้นตอนที่ 4: เพิ่ม RunBlockTest
ต้องใช้ข้อผิดพลาด Coroutine เนื่องจาก getTasks
เป็นฟังก์ชัน suspend
และคุณต้องเปิด Coroutine เพื่อเรียกใช้งาน คุณจึงต้องมีขอบเขต Coroutine หากต้องการแก้ไขข้อผิดพลาดนี้ คุณจะต้องเพิ่มทรัพยากร Dependency บางส่วนเพื่อรับมือกับการเปิด Coroutine ในการทดสอบ
- เพิ่มทรัพยากร Dependency ที่จําเป็นสําหรับการทดสอบ Coroutine ให้กับแหล่งที่มาของการทดสอบที่กําหนดโดยใช้
testImplementation
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
อย่าลืมซิงค์
kotlinx-coroutines-test
เป็นไลบรารีการทดสอบ Coroutine โดยเฉพาะสําหรับการทดสอบ Coroutine หากต้องการเรียกใช้การทดสอบ ให้ใช้ฟังก์ชัน runBlockingTest
นี่คือฟังก์ชันการทํางานจากไลบรารีการทดสอบ Coroutine โดยจะใช้บล็อกโค้ดแล้วเรียกใช้โค้ดนี้ในโค้ด Coroutine พิเศษ ซึ่งจะทํางานพร้อมกันทันที ซึ่งหมายความว่าการดําเนินการจะเกิดขึ้นตามลําดับที่กําหนด การดําเนินการนี้จะทําให้ Coroutine ทํางานแบบไม่เป็น Coroutine จึงมีไว้เพื่อทดสอบโค้ด
ใช้ runBlockingTest
ในชั้นเรียนทดสอบเมื่อเรียกใช้ฟังก์ชัน suspend
ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทํางานของ runBlockingTest
และวิธีทดสอบคอร์คอร์ทีใน Codelab ชุดถัดไปได้
- เพิ่ม
@ExperimentalCoroutinesApi
เหนือชั้นเรียน ข้อผิดพลาดนี้แสดงว่าคุณใช้ API Coroutine (runBlockingTest
) ในการทดสอบในชั้นเรียน หากไม่มี ก็จะได้รับคําเตือน - กลับไปยัง
DefaultTasksRepositoryTest
เพิ่มrunBlockingTest
ลงในการทดสอบทั้งหมดเป็น "block" ของโค้ด
การทดสอบสุดท้ายนี้มีลักษณะเหมือนโค้ดด้านล่าง
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}
- ทดสอบ
getTasks_requestsAllTasksFromRemoteDataSource
ใหม่และยืนยันว่าใช้งานได้ และข้อผิดพลาดหายไปแล้ว
คุณเพิ่งเห็นวิธีทดสอบที่เก็บของ ในขั้นตอนต่อไป คุณจะได้ใช้การแทรกทรัพยากร Dependency อีกครั้งและสร้างการทดสอบใหม่ 1 ครั้ง ที่จะแสดงวิธีเขียนการทดสอบหน่วยและการผสานรวมสําหรับโมเดลข้อมูลพร็อพเพอร์ตี้
การทดสอบหน่วยควรทดสอบเฉพาะชั้นเรียนหรือวิธีการที่คุณสนใจเท่านั้น สิ่งนี้เรียกว่าการทดสอบในการแยก ซึ่งคุณสามารถแยกโค้ด "unit" ได้อย่างชัดเจน และทดสอบเฉพาะโค้ดที่เป็นส่วนหนึ่งของหน่วยนั้นเท่านั้น
ดังนั้น TasksViewModelTest
ควรทดสอบโค้ด TasksViewModel
เท่านั้น จึงไม่ควรทดสอบในฐานข้อมูล เครือข่าย หรือคลาสที่เก็บ ดังนั้น สําหรับโมเดลข้อมูลพร็อพเพอร์ตี้ เช่นเดียวกับที่คุณเพิ่งสร้างสําหรับที่เก็บ คุณจะสร้างที่เก็บปลอมและใช้การแทรกทรัพยากร Dependency เพื่อนําไปใช้ในการทดสอบ
ในงานนี้ คุณจะได้ใช้การแทรกการขึ้นต่อกันเพื่อดูโมเดล
ขั้นตอนที่ 1 สร้างอินเทอร์เฟซ TasksRepository
ขั้นตอนแรกในการใช้การแทรกตัวสร้างเครื่องมือสร้างคือการสร้างอินเทอร์เฟซทั่วไปที่ใช้ร่วมกันระหว่างคลาสปลอมและคลาสจริง
สิ่งนี้มีลักษณะอย่างไรในทางปฏิบัติ ดู TasksRemoteDataSource
, TasksLocalDataSource
และ FakeDataSource
แล้วสังเกตว่าทั้งหมดมีอินเทอร์เฟซเดียวกัน: TasksDataSource
ซึ่งจะช่วยให้คุณพูดในตัวสร้าง DefaultTasksRepository
ที่ถ่ายใน TasksDataSource
ได้
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
นี่คือสิ่งที่ทําให้เราได้สลับใช้ FakeDataSource
ของคุณ
ต่อไป ให้สร้างอินเทอร์เฟซสําหรับ DefaultTasksRepository
ตามที่คุณทํากับแหล่งข้อมูล และจําเป็นต้องรวมเมธอดสาธารณะทั้งหมด (แพลตฟอร์ม API สาธารณะ) ของ DefaultTasksRepository
- เปิด
DefaultTasksRepository
และคลิกขวาที่ชื่อชั้นเรียน จากนั้นเลือก Rector -> แยก -> Interface
- เลือกแตกไฟล์เพื่อแยกไฟล์
- เปลี่ยนชื่ออินเทอร์เฟซเป็น
TasksRepository
ในหน้าต่างแยกอินเทอร์เฟซ - ในส่วนสมาชิกในแบบฟอร์มการลงทะเบียน ให้เลือกสมาชิกทั้งหมดยกเว้นสมาชิกร่วม 2 คนและวิธีการส่วนตัว
- คลิกเปลี่ยนโครงสร้าง อินเทอร์เฟซ
TasksRepository
ใหม่ควรปรากฏในแพ็กเกจข้อมูล/แหล่งที่มา
และ DefaultTasksRepository
ใช้งาน TasksRepository
แล้ว
- เรียกใช้แอป (ไม่ใช่การทดสอบ) เพื่อให้แน่ใจว่าทุกอย่างยังคงทํางานได้ตามปกติ
ขั้นตอนที่ 2 สร้าง FakeTestRepository
เมื่อมีอินเทอร์เฟซแล้ว คุณก็สร้างการทดสอบ DefaultTaskRepository
ได้เลย
- ในแหล่งที่มาของชุดแหล่งที่มา ในข้อมูล/แหล่งที่มา ให้สร้างไฟล์ Kotlin และคลาส
FakeTestRepository.kt
และขยายจากอินเทอร์เฟซTasksRepository
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
ระบบจะแจ้งให้คุณติดตั้งใช้งานเมธอดของอินเทอร์เฟซ
- วางเมาส์เหนือข้อผิดพลาดจนกว่าจะเห็นเมนูคําแนะนํา แล้วคลิกใช้สมาชิก
- เลือกวิธีทั้งหมดและกดตกลง
ขั้นตอนที่ 3 ใช้เมธอด FakeTestRepository
ตอนนี้คุณมีคลาส FakeTestRepository
ที่มีวิธี "ไม่ได้ใช้" โครงสร้างของ FakeTestRepository
จะรองรับโดยโครงสร้างสื่อกลาง แทนที่จะใช้สื่อกลางแบบซับซ้อนระหว่างแหล่งข้อมูลในพื้นที่และทางไกล เช่นเดียวกับการใช้ FakeDataSource
โปรดทราบว่า FakeTestRepository
ไม่จําเป็นต้องใช้ FakeDataSource
หรืออะไรทํานองนั้น เพียงแต่ต้องแสดงผลเอาต์พุตปลอมตามจริงตามจริงในอินพุต คุณจะใช้ LinkedHashMap
เพื่อจัดเก็บรายการงานและใช้ MutableLiveData
สําหรับงานที่คุณสังเกตได้
- ใน
FakeTestRepository
ให้เพิ่มทั้งตัวแปรLinkedHashMap
ที่แสดงถึงรายการงานปัจจุบันและMutableLiveData
สําหรับงานที่สังเกตได้
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
ใช้วิธีการต่อไปนี้
getTasks
- เมธอดนี้ควรใช้tasksServiceData
และเปลี่ยนเป็นรายการโดยใช้tasksServiceData.values.toList()
และแสดงผลเป็นSuccess
refreshTasks
- อัปเดตค่าของobservableTasks
ให้เป็นสิ่งที่getTasks()
แสดงผลobserveTasks
- สร้างโครูทีนโดยใช้runBlocking
และเรียกใช้refreshTasks
จากนั้นแสดงผลobservableTasks
ด้านล่างนี้เป็นโค้ดสําหรับวิธีการดังกล่าว
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}
ขั้นตอนที่ 4 เพิ่มวิธีทดสอบลงใน addTasks
เมื่อทดสอบ ควรมี Tasks
อยู่ในที่เก็บอยู่แล้ว คุณจะโทรหา saveTask
ได้หลายครั้ง แต่หากต้องการทําให้วิธีนี้ง่ายขึ้น ให้เพิ่มวิธีตัวช่วยเฉพาะสําหรับการทดสอบที่ให้คุณเพิ่มงาน
- เพิ่มเมธอด
addTasks
ซึ่งจะใช้vararg
งานแล้วเพิ่มแต่ละงานลงในHashMap
แล้วรีเฟรชงาน
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
ในตอนนี้ คุณมีที่เก็บปลอมสําหรับการทดสอบโดยใช้วิธีการหลักๆ 2-3 วิธี ต่อไปให้ใช้สิ่งนี้ในการทดสอบ
ในงานนี้ คุณจะใช้ชั้นเรียนปลอมใน ViewModel
ใช้การแทรกทรัพยากร Dependency เพื่อใส่แหล่งข้อมูล 2 รายการผ่านการแทรกการขึ้นต่อกันของเครื่องมือสร้างโดยเพิ่มตัวแปร TasksRepository
ในเครื่องมือสร้าง TasksViewModel
'
ขั้นตอนนี้แตกต่างไปเล็กน้อยเมื่อใช้รูปแบบข้อมูลพร็อพเพอร์ตี้เนื่องจากคุณไม่ได้สร้างรูปแบบโดยตรง ดังตัวอย่างต่อไปนี้
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
ดังที่โค้ดข้างต้นใช้การมอบสิทธิ์พร็อพเพอร์ตี้ viewModel's
ของโมเดลการสร้างรูปแบบ หากต้องการเปลี่ยนวิธีสร้างรูปแบบมุมมอง คุณต้องเพิ่มและใช้ ViewModelProvider.Factory
หากคุณไม่คุ้นเคยกับ ViewModelProvider.Factory
ดูข้อมูลเพิ่มเติมได้ที่นี่
ขั้นตอนที่ 1 สร้างและใช้ ViewModelFactory ใน TasksViewModel
คุณจะเริ่มจากการอัปเดตชั้นเรียนและทดสอบที่เกี่ยวข้องกับหน้าจอ Tasks
- เปิด
TasksViewModel
- เปลี่ยนเครื่องมือสร้าง
TasksViewModel
เพื่อใช้ในTasksRepository
แทนที่จะสร้างภายในชั้นเรียน
TasksViewModel.kt
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}
เนื่องจากคุณเปลี่ยนเครื่องมือสร้าง จึงต้องใช้โรงงานเพื่อสร้าง TasksViewModel
ใส่ชั้นเรียนเริ่มต้นในไฟล์เดียวกับ TasksViewModel
หรือใส่ไว้ในไฟล์ของตัวเองก็ได้
- ที่ด้านล่างของไฟล์
TasksViewModel
ให้เพิ่มTasksViewModelFactory
ซึ่งใช้ในTasksRepository
ธรรมดานอกชั้นเรียน
TasksViewModel.kt
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
นี่เป็นวิธีมาตรฐานที่คุณเปลี่ยนวิธีสร้าง ViewModel
ตอนนี้คุณมีโรงงานแล้ว คุณสามารถใช้เป็นค่าเริ่มต้นเมื่อคุณสร้างโมเดลข้อมูลพร็อพเพอร์ตี้
- อัปเดต
TasksFragment
เพื่อใช้จากโรงงาน
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- เรียกใช้โค้ด app ของคุณและตรวจสอบให้แน่ใจว่าทุกอย่างยังทํางานได้อยู่
ขั้นตอนที่ 2 ใช้ FakeTestRepository ภายใน TasksViewModelTest
ตอนนี้คุณสามารถใช้ที่เก็บปลอมแทนการใช้ที่เก็บจริงในการทดสอบโมเดลข้อมูลพร็อพเพอร์ตี้ได้
- เปิด
TasksViewModelTest
- เพิ่มพร็อพเพอร์ตี้
FakeTestRepository
ในTasksViewModelTest
TaskViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeTestRepository
// Rest of class
}
- อัปเดตเมธอด
setupViewModel
เพื่อสร้างFakeTestRepository
ที่มี 3 งาน แล้วสร้างtasksViewModel
ด้วยที่เก็บนี้
TasksViewModelTest.kt
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}
- เนื่องจากคุณไม่ได้ใช้โค้ดการทดสอบ AndroidX
ApplicationProvider.getApplicationContext
แล้ว คุณจึงนําคําอธิบายประกอบ@RunWith(AndroidJUnit4::class)
ออกได้ด้วย - ทําการทดสอบให้แน่ใจว่าการทดสอบยังทํางานอยู่
การใช้การแทรกทรัพยากร Dependency เครื่องมือสร้างหมายความว่าคุณได้นํา DefaultTasksRepository
ที่เป็นทรัพยากร Dependency ออก แล้วแทนที่ด้วย FakeTestRepository
ในการทดสอบ
ขั้นตอนที่ 3 อัปเดต TaskDetail Fragment และ Viewmodel ด้วย
ทําการเปลี่ยนแปลงที่เหมือนกันทุกประการสําหรับ TaskDetailFragment
และ TaskDetailViewModel
การดําเนินการนี้จะเตรียมรหัสเมื่อคุณเขียนการทดสอบ TaskDetail
รายการถัดไป
- เปิด
TaskDetailViewModel
- วิธีอัปเดตเครื่องมือสร้าง
TaskDetailViewmodel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
- ที่ด้านล่างของไฟล์
TaskDetailViewModel
ให้เพิ่มTaskDetailViewModelFactory
ภายนอกชั้นเรียน
TaskDetailViewmodel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}
- อัปเดต
TasksFragment
เพื่อใช้จากโรงงาน
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- เรียกใช้โค้ดและตรวจสอบว่าทุกอย่างใช้งานได้
ตอนนี้คุณใช้ FakeTestRepository
แทนที่เก็บจริงใน TasksFragment
และ TasksDetailFragment
ได้แล้ว
ถัดไป คุณจะเขียนการทดสอบการผสานรวมเพื่อทดสอบการโต้ตอบกับส่วนย่อยและโมเดลการดู คุณจะดูได้ว่าโค้ดโมเดลการดูของคุณอัปเดต UI อย่างเหมาะสมหรือไม่ หากต้องการทําเช่นนี้ ให้ใช้
- รูปแบบ ServiceLocator
- ห้องสมุด Espresso และ Mockito
การทดสอบการผสานรวมจะทดสอบการโต้ตอบของคลาสต่างๆ เพื่อให้แน่ใจว่าลักษณะการทํางานตามที่คาดไว้เมื่อใช้ร่วมกัน คุณสามารถเรียกใช้การทดสอบเหล่านี้ในเครื่อง (ชุดแหล่งที่มา test
ชุด) หรือการทดสอบการวัด (ชุดแหล่งที่มา androidTest
ชุด)
ในกรณีของคุณ คุณจะได้ทดสอบส่วนย่อยและเขียนการผสานรวมส่วนย่อยสําหรับโมเดลส่วนย่อยและการดูเพื่อทดสอบฟีเจอร์หลักของส่วนย่อย
ขั้นตอนที่ 1 เพิ่มทรัพยากร Dependency ของ Gradle
- เพิ่มทรัพยากร Dependency ของ Gradle ต่อไปนี้
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
โดยทรัพยากร Dependency เหล่านี้ ได้แก่
junit:junit
-JUnit ซึ่งจําเป็นสําหรับการเขียนคําสั่งทดสอบพื้นฐานandroidx.test:core
- ไลบรารีการทดสอบ AndroidX หลักkotlinx-coroutines-test
- ไลบรารีการทดสอบ Coroutineandroidx.fragment:fragment-testing
- ไลบรารีการทดสอบ AndroidX สําหรับสร้างส่วนย่อยในการทดสอบและเปลี่ยนสถานะ
เนื่องจากคุณจะใช้ไลบรารีเหล่านี้ในชุดแหล่งที่มาของ androidTest
โปรดใช้ androidTestImplementation
เพื่อเพิ่มทรัพยากรเหล่านั้นเป็นทรัพยากร Dependency
ขั้นตอนที่ 2 สร้างคลาส TaskDetailFragmentTest
TaskDetailFragment
จะแสดงข้อมูลเกี่ยวกับงานเดียว
คุณจะเริ่มต้นด้วยการเขียนส่วนย่อยสําหรับ TaskDetailFragment
เนื่องจากมีฟังก์ชันพื้นฐานค่อนข้างเมื่อเทียบกับส่วนย่อยอื่นๆ
- เปิด
taskdetail.TaskDetailFragment
- สร้างการทดสอบ
TaskDetailFragment
เหมือนที่เคยทําก่อนหน้านี้ ยอมรับตัวเลือกเริ่มต้นและวางไว้ในชุดแหล่งที่มา androidTest (ไม่ใช่ชุดแหล่งที่มาtest
)
- เพิ่มคําอธิบายประกอบต่อไปนี้ไปยังคลาส
TaskDetailFragmentTest
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
วัตถุประสงค์ของคําอธิบายประกอบเหล่านี้คือ
@MediumTest
- ทําเครื่องหมายการทดสอบว่าเป็นการทดสอบการผสานรวม &" ขนาดกลาง (การทดสอบเทียบกับหน่วย@SmallTest
และการทดสอบแบบจุดต่อจุด@LargeTest
) วิธีนี้จะช่วยให้คุณจัดกลุ่มและเลือกขนาดการทดสอบที่จะเรียกใช้ได้@RunWith(AndroidJUnit4::class)
- ใช้ในชั้นเรียนใดก็ได้ที่ใช้ AndroidX Test
ขั้นตอนที่ 3 เปิดส่วนย่อยจากการทดสอบ
ในงานนี้ คุณจะต้องเปิดตัว TaskDetailFragment
โดยใช้ไลบรารีการทดสอบ AndroidX FragmentScenario
เป็นคลาสจากการทดสอบ AndroidX ที่รวมส่วนย่อยและทําให้คุณควบคุมอายุการใช้งานของส่วนย่อยและการทดสอบได้โดยตรง หากต้องการเขียนการทดสอบสําหรับส่วนย่อย คุณจะต้องสร้าง FragmentScenario
สําหรับส่วนย่อยที่คุณกําลังทดสอบ (TaskDetailFragment
)
- คัดลอกการทดสอบนี้ไปยัง
TaskDetailFragmentTest
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
โค้ดด้านบน:
- สร้างงาน
- สร้าง
Bundle
ซึ่งแสดงถึงอาร์กิวเมนต์ส่วนย่อยสําหรับงานที่ส่งผ่านไปยังส่วนย่อย - ฟังก์ชัน
launchFragmentInContainer
จะสร้างFragmentScenario
ที่มีแพ็กเกจและธีมนี้
ข้อมูลนี้ยังไม่ใช่การทดสอบที่เสร็จสมบูรณ์เนื่องจากยังไม่ยืนยัน สําหรับตอนนี้ ให้ทําการทดสอบและสังเกตดูว่าอะไรเกิดขึ้น
- นี่คือการทดสอบการวัดคุม ดังนั้นให้ตรวจสอบว่าโปรแกรมจําลองหรืออุปกรณ์มองเห็นได้
- เรียกใช้การทดสอบ
สิ่งที่ควรดําเนินการ
- ก่อนอื่น เนื่องจากนี่เป็นการทดสอบการวัดคุม การทดสอบจะทํางานในอุปกรณ์จริง (หากเชื่อมต่ออยู่) หรือโปรแกรมจําลอง
- จากนั้นระบบจะเปิดส่วนย่อย
- สังเกตดูว่าไม่มีการนําทางไปยังส่วนย่อยอื่นๆ หรือมีเมนูใดๆ ที่เชื่อมโยงกับกิจกรรมมากน้อยเพียงใด แต่เป็นเฉพาะส่วนย่อย
สุดท้าย ให้สังเกตโดยละเอียดว่าส่วนย่อยมีข้อความว่า "" ไม่มีข้อมูล " เนื่องจากเป็นการโหลดข้อมูลงานไม่สําเร็จ
การทดสอบทั้ง 2 รายการต้องโหลด TaskDetailFragment
(ซึ่งคุณทําได้) และยืนยันว่าข้อมูลโหลดอย่างถูกต้อง ทําไมไม่มีข้อมูล เนื่องจากคุณได้สร้างงาน แต่ไม่ได้บันทึกในที่เก็บ
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
คุณมีไฟล์ FakeTestRepository
นี้ แต่ต้องหาวิธีแทนที่ที่เก็บจริงด้วยไฟล์ปลอมสําหรับส่วนย่อยของคุณ คุณจะทําแบบนี้ต่อไปเรื่อยๆ
ในงานนี้ คุณจะต้องใส่ที่เก็บปลอมลงในส่วนย่อยของคุณโดยใช้ ServiceLocator
วิธีนี้ช่วยให้คุณเขียนส่วนย่อยและดูการทดสอบการผสานรวมโมเดลได้
คุณใช้การแทรกทรัพยากร Dependency ในเครื่องมือสร้างที่นี่ไม่ได้เหมือนที่ก่อนหน้านี้ทําเมื่อคุณจําเป็นต้องพึ่งโมเดลการดูหรือที่เก็บ คุณต้องสร้างคลาสเพื่อแทรกการขึ้นต่อกันของเครื่องมือสร้าง Fragment และกิจกรรมต่างๆ เป็นตัวอย่างของคลาสที่คุณไม่ได้สร้าง โดยทั่วไปจะไม่มีสิทธิ์เข้าถึงเครื่องมือสร้าง
เนื่องจากคุณไม่ได้สร้างส่วนย่อย คุณจึงใช้การแทรกทรัพยากร Dependency ของตัวสร้างเพื่อสลับระหว่างการทดสอบที่เก็บ (FakeTestRepository
) กับส่วนย่อยได้ ให้ใช้รูปแบบ ตัวระบุตําแหน่งบริการแทน รูปแบบตัวระบุตําแหน่งบริการเป็นอีกทางเลือกสําหรับการแทรกการขึ้นต่อกัน โดยเกี่ยวข้องกับการสร้างคลาสเดี่ยวที่เรียกว่า "Service Locator" โดยมีจุดประสงค์เพื่ออ้างอิงทรัพยากร Dependency ทั้งสําหรับโค้ดปกติและโค้ดทดสอบ ในรหัสแอปปกติ (ชุดแหล่งที่มา main
) ทรัพยากร Dependency ทั้งหมดเหล่านี้จะขึ้นอยู่กับทรัพยากร Dependency ของแอปปกติ สําหรับการทดสอบ คุณจะแก้ไขตัวระบุตําแหน่งบริการเพื่อให้บริการทรัพยากร Dependency ของเวอร์ชันซ้ําได้
ไม่ได้ใช้ตัวระบุตําแหน่งบริการ | การใช้ตัวระบุตําแหน่งบริการ |
สําหรับแอป Codelab นี้ ให้ทําดังนี้
- สร้างคลาสตัวระบุตําแหน่งบริการที่สร้างและจัดเก็บที่เก็บได้ โดยค่าเริ่มต้น ระบบจะสร้างที่เก็บ "normal"
- เปลี่ยนโครงสร้างโค้ดเพื่อให้ใช้ที่เก็บบริการได้เมื่อต้องใช้ที่เก็บ
- ในชั้นเรียนทดสอบ ให้เรียกใช้เมธอดบน Service Locator ซึ่งจะสลับที่เก็บ "normal" กับการทดสอบสองครั้ง
ขั้นตอนที่ 1 สร้าง ServiceLocator
มาสร้างชั้นเรียน ServiceLocator
กัน และจะอยู่ในแหล่งที่มาหลักที่ตั้งค่าไว้ด้วยโค้ดของแอปอื่นๆ เพราะใช้โดยโค้ดแอปพลิเคชันหลัก
หมายเหตุ: ServiceLocator
เป็นรายการเดียว คุณจึงควรใช้คีย์เวิร์ด Kotlin object
สําหรับชั้นเรียน
- สร้างไฟล์ ServiceLocator.kt ในระดับบนสุดของชุดแหล่งที่มาหลัก
- กําหนด
object
ชื่อServiceLocator
- สร้างตัวแปรอินสแตนซ์
database
และrepository
และตั้งค่าเป็นnull
- เพิ่มคําอธิบายประกอบในที่เก็บด้วย
@Volatile
เนื่องจากอาจมีหลายชุดข้อความ (@Volatile
มีคําอธิบายโดยละเอียดที่นี่)
โค้ดของคุณควรมีลักษณะดังที่แสดงด้านล่าง
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
ตอนนี้สิ่งเดียวที่ ServiceLocator
ต้องทําคือวิธีแสดง TasksRepository
และจะส่งคืน DefaultTasksRepository
ที่มีอยู่แล้ว หรือส่งคืน DefaultTasksRepository
ใหม่หากจําเป็น
กําหนดฟังก์ชันต่อไปนี้
provideTasksRepository
- ให้ที่เก็บที่มีอยู่หรือสร้างใหม่ เมธอดนี้ควรเป็นsynchronized
ในthis
เพื่อหลีกเลี่ยงสถานการณ์ที่มีชุดข้อความหลายชุด และเผลอสร้างอินสแตนซ์ที่เก็บข้อมูล 2 รายการcreateTasksRepository
- โค้ดสําหรับสร้างที่เก็บใหม่ จะเรียกcreateTaskLocalDataSource
และสร้างTasksRemoteDataSource
ใหม่createTaskLocalDataSource
- โค้ดสําหรับสร้างแหล่งข้อมูลท้องถิ่นใหม่ จะโทรหาcreateDataBase
createDataBase
- โค้ดสําหรับสร้างฐานข้อมูลใหม่
รหัสที่สมบูรณ์:
ServiceLocator.kt
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}
ขั้นตอนที่ 2 ใช้ ServiceLocator ในแอปพลิเคชัน
ระบบจะทําการเปลี่ยนแปลงในโค้ดแอปพลิเคชันหลัก (ไม่ใช่การทดสอบ) เพื่อสร้างที่เก็บในที่เดียวคือ ServiceLocator
สิ่งสําคัญคือคุณจะต้องสร้างที่เก็บที่เก็บเพียงครั้งเดียวเท่านั้น คุณต้องใช้ตัวระบุตําแหน่งบริการในแอปพลิเคชันประเภทของฉันเพื่อให้ดําเนินการดังกล่าว
- ในระดับบนสุดของลําดับชั้นแพ็กเกจ ให้เปิด
TodoApplication
แล้วสร้างval
สําหรับที่เก็บและกําหนดที่เก็บที่ได้รับจากServiceLocator.provideTaskRepository
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
เมื่อสร้างที่เก็บในแอปพลิเคชันแล้ว คุณสามารถนําวิธีการ getRepository
เก่าออกได้ใน DefaultTasksRepository
- เปิด
DefaultTasksRepository
และลบออบเจ็กต์โฆษณาที่แสดงร่วม
DefaultTasksRepository.kt
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
ตอนนี้ไม่ว่าคุณจะใช้ getRepository
ที่ไหน ให้ใช้แอปพลิเคชัน taskRepository
# แทน จึงทําให้แทนที่คุณจะได้รับที่เก็บโดยตรงใน ServiceLocator
แทนที่คุณจะได้รับที่เก็บใดๆ ก็ตาม
- เปิด
TaskDetailFragement
แล้วค้นหาการโทรไปยังgetRepository
ที่ด้านบนของชั้นเรียน - แทนที่สายนี้ด้วยสายที่รับที่เก็บจาก
TodoApplication
TaskDetailFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
- ให้ทําแบบเดียวกันนี้สําหรับ
TasksFragment
TasksFragment.kt
// REPLACE this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
- สําหรับ
StatisticsViewModel
และAddEditTaskViewModel
ให้อัปเดตรหัสที่ได้มาจากที่เก็บเพื่อใช้ที่เก็บจากTodoApplication
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- เรียกใช้แอปพลิเคชัน (ไม่ใช่การทดสอบ)
เนื่องจากคุณเปลี่ยนโครงสร้างแอปเท่านั้น แอปจึงควรทํางานแบบเดิมโดยไม่มีปัญหา
ขั้นตอนที่ 3 สร้าง FakeAndroidTestRepository
คุณมี FakeTestRepository
ในชุดแหล่งที่มาของการทดสอบอยู่แล้ว คุณแชร์คลาสทดสอบระหว่างชุดแหล่งที่มา test
และ androidTest
ไม่ได้โดยค่าเริ่มต้น คุณจึงต้องสร้างชั้นเรียน FakeTestRepository
ที่ซ้ํากันในชุดแหล่งที่มา androidTest
โดยให้เรียกว่า FakeAndroidTestRepository
- คลิกขวาชุดแหล่งที่มา
androidTest
แล้วสร้างแพ็กเกจข้อมูล คลิกขวาอีกครั้งและสร้างแพ็กเกจแหล่งที่มา - สร้างคลาสใหม่ในแพ็กเกจแหล่งที่มานี้ที่ชื่อ
FakeAndroidTestRepository.kt
- คัดลอกโค้ดต่อไปนี้ไปยังชั้นเรียนนั้น
FakeAndroidTestRepository.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
ขั้นตอนที่ 4 เตรียม ServiceLocator สําหรับการทดสอบ
โอเค เวลาที่ใช้ ServiceLocator
เพื่อเปลี่ยนการทดสอบเป็น 2 เท่าเมื่อทดสอบ โดยต้องเพิ่มโค้ดลงในโค้ด ServiceLocator
- เปิด
ServiceLocator.kt
- ทําเครื่องหมายตัวตั้งค่าสําหรับ
tasksRepository
เป็น@VisibleForTesting
คําอธิบายประกอบนี้เป็นวิธีชี้แจงว่าเหตุผลที่ผู้ตั้งค่าเป็นแบบสาธารณะเนื่องจากการทดสอบ
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
ไม่ว่าคุณจะทําการทดสอบคนเดียวหรือเป็นกลุ่มทดสอบ การทดสอบก็ควรทํางานเหมือนกัน ซึ่งหมายความว่าการทดสอบไม่ควรมีลักษณะการทํางานที่พึ่งพากันและกัน (ซึ่งหมายถึงการหลีกเลี่ยงการแชร์ออบเจ็กต์ระหว่างการทดสอบ)
เนื่องจาก ServiceLocator
เป็นบรรทัดเดียว จึงมีโอกาสถูกแชร์ระหว่างการทดสอบโดยไม่ได้ตั้งใจ สร้างการรีเซ็ตสถานะ ServiceLocator
ระหว่างการทดสอบอย่างเหมาะสมเพื่อหลีกเลี่ยงปัญหานี้
- เพิ่มตัวแปรอินสแตนซ์ชื่อ
lock
ที่มีค่าAny
ServiceLocator.kt
private val lock = Any()
- เพิ่มเมธอดเฉพาะการทดสอบที่เรียกว่า
resetRepository
ซึ่งจะล้างฐานข้อมูลและตั้งค่าทั้งที่เก็บและฐานข้อมูลเป็นค่าว่าง
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}
ขั้นตอนที่ 5 ใช้ ServiceLocator
ในขั้นตอนนี้ ให้ใช้ ServiceLocator
- เปิด
TaskDetailFragmentTest
- ประกาศตัวแปร
lateinit TasksRepository
- เพิ่มการตั้งค่าและวิธีลดหย่อนเพื่อตั้งค่า
FakeAndroidTestRepository
ก่อนการทดสอบแต่ละครั้ง แล้วทําความสะอาดหลังการทดสอบแต่ละครั้ง
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- รวมเนื้อหาฟังก์ชันของ
activeTaskDetails_DisplayedInUi()
ในrunBlockingTest
- บันทึก
activeTask
ในที่เก็บก่อนเปิดใช้ส่วนย่อย
repository.saveTask(activeTask)
การทดสอบสุดท้ายมีลักษณะคล้ายโค้ดนี้ด้านล่าง
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
- ใส่คําอธิบายประกอบให้กับทั้งชั้นเรียนด้วย
@ExperimentalCoroutinesApi
เมื่อเสร็จแล้ว โค้ดจะมีลักษณะดังนี้
TaskDetailFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- ทําการทดสอบ
activeTaskDetails_DisplayedInUi()
เหมือนเช่นเคย คุณควรเห็นส่วนย่อยยกเว้นเวลานี้ เนื่องจากคุณได้ตั้งค่าที่เก็บอย่างถูกต้อง ตอนนี้ระบบจึงแสดงข้อมูลงาน
ในขั้นตอนนี้ คุณจะใช้ไลบรารีการทดสอบ UI ของ Espresso เพื่อทําการทดสอบการผสานรวมรายการแรกให้เสร็จสมบูรณ์ คุณจัดโครงสร้างโค้ดแล้วเพื่อให้เพิ่มการทดสอบที่มีการยืนยันสําหรับ UI ได้ ในการดําเนินการดังกล่าว คุณจะใช้ไลบรารีการทดสอบเอสเปรสโซ
เอสเปรสโซช่วยให้คุณทําสิ่งต่อไปนี้ได้
- โต้ตอบกับการดู เช่น การคลิกปุ่ม แถบเลื่อน หรือการเลื่อนหน้าจอ
- ยืนยันว่ามุมมองบางอย่างอยู่บนหน้าจอหรืออยู่ในสถานะบางอย่าง (เช่น มีข้อความเฉพาะ หรือมีการเลือกช่องทําเครื่องหมาย)
ขั้นตอนที่ 1 การขึ้นต่อกันของ Gradle
คุณจะได้รับทรัพยากร Dependo หลักอยู่แล้วเนื่องจากรวมอยู่ในโปรเจ็กต์ Android โดยค่าเริ่มต้น
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
- ทรัพยากรหลักใน Espresso นี้จะรวมอยู่ด้วยโดยค่าเริ่มต้นเมื่อคุณสร้างโปรเจ็กต์ Android ใหม่ ซึ่งประกอบด้วยโค้ดการทดสอบพื้นฐานสําหรับข้อมูลพร็อพเพอร์ตี้และการกระทําส่วนใหญ่
ขั้นตอนที่ 2 ปิดภาพเคลื่อนไหว
การทดสอบด้วยเอสเปรสโซจะทํางานบนอุปกรณ์จริง ดังนั้นจึงเป็นการทดสอบการวัดคุมโดยธรรมชาติ ปัญหาหนึ่งที่เกิดขึ้นคือภาพเคลื่อนไหว: หากภาพเคลื่อนไหวไม่เป็นไปตามกําหนดและคุณพยายามทดสอบว่าการดูนั้นบนหน้าจอหรือไม่ แต่ขณะนี้ภาพเคลื่อนไหวยังคงเคลื่อนไหวได้ Espresso อาจทดสอบไม่สําเร็จ เนื่องจากอาจทําให้การทดสอบเอสเพรสโซไม่น่าเชื่อถือ
การทดสอบ UI ของ Espresso แนวทางปฏิบัติที่ดีที่สุดในการปิดภาพเคลื่อนไหว (คือการทดสอบจะเร็วขึ้นด้วย):
- ในอุปกรณ์ทดสอบ ให้ไปที่การตั้งค่า > ตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์
- ปิดใช้การตั้งค่า 3 อย่างนี้ ได้แก่ ขนาดภาพเคลื่อนไหวของหน้าต่าง ขนาดภาพเคลื่อนไหวการเปลี่ยน และขนาดระยะเวลาของภาพเคลื่อนไหว
ขั้นตอนที่ 3 ดูการทดสอบเอสเปรสโซ
ก่อนที่จะเขียนการทดสอบเอสเปรสโซ โปรดดูรหัสเอสเปรสโซ
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
คําสั่งนี้จะค้นหามุมมองช่องทําเครื่องหมายที่มีรหัส task_detail_complete_checkbox
จากนั้นคลิกยืนยันว่าได้ตรวจสอบแล้ว
ข้อความส่วนใหญ่ของเอสเปรสโซประกอบด้วย 4 ส่วน ได้แก่
onView
onView
เป็นตัวอย่างของเมธอด Espresso แบบคงที่ที่เริ่มคําสั่ง Espresso onView
เป็นหนึ่งในตัวเลือกที่พบบ่อยที่สุด แต่มีตัวเลือกอื่นๆ เช่น onData
2. View Matcher
withId(R.id.task_detail_title_text)
withId
เป็นตัวอย่างของ ViewMatcher
ซึ่งได้รับยอดดูตามรหัส โดยจะมีตัวจับคู่มุมมองอื่นในเอกสารประกอบ
3. ViewAction
perform(click())
เมธอด perform
ที่ใช้ ViewAction
ViewAction
คือสิ่งที่มุมมองทําได้ เช่น การคลิกที่นี่จะคลิกมุมมอง
check(matches(isChecked()))
check
ซึ่งจะมี ViewAssertion
ViewAssertion
จะตรวจสอบหรือยืนยันข้อมูลบางอย่างเกี่ยวกับข้อมูลพร็อพเพอร์ตี้ ViewAssertion
ที่พบบ่อยที่สุดที่คุณจะใช้คือการยืนยัน matches
หากต้องการยืนยันให้เสร็จสมบูรณ์ ให้ใช้ ViewMatcher
อื่นในกรณีนี้ isChecked
โปรดทราบว่าคุณไม่จําเป็นต้องเรียกทั้ง perform
และ check
ในคําสั่งเอสเปรสโซเสมอไป คุณสามารถข้อความที่เพียงแค่ยืนยันโดยใช้ check
หรือเพียงแค่ ViewAction
โดยใช้ perform
- เปิด
TaskDetailFragmentTest.kt
- อัปเดตการทดสอบ
activeTaskDetails_DisplayedInUi
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
นี่คือคําสั่งนําเข้า หากจําเป็น
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
- ทุกอย่างที่อยู่หลังความคิดเห็นของ
// THEN
จะใช้เอสเปรสโซ ตรวจสอบโครงสร้างการทดสอบและการใช้งานwithId
และตรวจสอบเพื่อยืนยันความถูกต้องของหน้ารายละเอียด - ทําการทดสอบและยืนยันว่าผ่าน
ขั้นตอนที่ 4 (ไม่บังคับ) เขียนการทดสอบเอสเปรสโซของคุณเอง
ให้เขียนการทดสอบเอง
- สร้างการทดสอบใหม่ชื่อ
completedTaskDetails_DisplayedInUi
และคัดลอกโค้ดโครงกระดูกนี้
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}
- เมื่อดูที่การทดสอบก่อนหน้า ให้ทําการทดสอบนี้ให้เสร็จสมบูรณ์
- เรียกใช้และยืนยันการทดสอบผ่าน
completedTaskDetails_DisplayedInUi
ที่เสร็จสมบูรณ์ควรจะเป็นโค้ดนี้
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}
ในขั้นตอนสุดท้าย คุณจะได้ดูวิธีทดสอบคอมโพเนนต์การนําทาง การใช้การทดสอบประเภทอื่นที่เรียกว่าการจําลอง รวมถึงไลบรารีการทดสอบ Mockito
ใน Codelab นี้ คุณได้ใช้การทดสอบที่เรียกว่า "ปลอม" 2 เท่า เฟกเป็นการทดสอบแบบ 1 ในหลายประเภท คุณควรใช้การทดสอบคู่ใดในการทดสอบคอมโพเนนต์การนําทาง
นึกถึงวิธีนําทาง ลองนึกภาพว่าคุณกําลังกดปุ่มใดงานหนึ่งใน TasksFragment
เพื่อไปยังหน้าจอรายละเอียดของงาน
โค้ดของ TasksFragment
ที่ชี้ไปยังหน้าจอรายละเอียดงานเมื่อมีการกด
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
การนําทางเกิดขึ้นเนื่องจากการเรียกเมธอด navigate
หากต้องการเขียนคําแถลงยืนยัน ไม่มีวิธีพิสูจน์ง่ายๆ ว่าได้ทดสอบการใช้ TaskDetailFragment
แล้วหรือยัง การไปยังส่วนต่างๆ คือการดําเนินการที่ซับซ้อนซึ่งไม่ทําให้เกิดเอาต์พุตหรือการเปลี่ยนแปลงสถานะที่ชัดเจน นอกเหนือไปจากการเริ่มต้นใช้งาน TaskDetailFragment
สิ่งที่ยืนยันได้คือวิธีเรียกใช้ navigate
ด้วยพารามิเตอร์การดําเนินการที่ถูกต้อง นี่คือสิ่งที่การทดสอบจําลองทําขึ้น 2 เท่า ซึ่งจะตรวจสอบว่าระบบเรียกใช้เมธอดใดหรือไม่
Mockito เป็นเฟรมเวิร์กสําหรับการทดสอบแบบคู่ แม้ว่าจะใช้คําจําลองใน API และชื่อไปแล้ว แต่ไม่ได้เป็นเพียงการล้อเลียนเท่านั้น นอกจากนี้ยังสร้างหลอดไฟและสอดแนมได้ด้วย
คุณจะใช้ Mockito เพื่อทําการเลียนแบบ NavigationController
ซึ่งยืนยันได้ว่ามีการเรียกเมธอดการนําทางอย่างถูกต้อง
ขั้นตอนที่ 1 เพิ่มทรัพยากร Dependency ของ Gradle
- เพิ่มทรัพยากร Dependency ของ Gradle
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core
- การขึ้นต่อกันแบบ Mockitodexmaker-mockito
- ไลบรารีนี้จะต้องใช้ Mockito ในโครงการ Android Mockito จําเป็นต้องสร้างชั้นเรียนตามรันไทม์ ใน Android จะใช้โค้ด Dex ไบต์ และไลบรารีนี้จะอนุญาตให้ Mockito สร้างออบเจ็กต์ระหว่างรันไทม์บน Androidandroidx.test.espresso:espresso-contrib
- ไลบรารีนี้ประกอบด้วยการมีส่วนร่วมภายนอก (ชื่อ) ซึ่งมีโค้ดทดสอบสําหรับมุมมองขั้นสูงขึ้น เช่นDatePicker
และRecyclerView
นอกจากนี้ยังมีการตรวจสอบการช่วยเหลือพิเศษและชั้นเรียนชื่อCountingIdlingResource
ซึ่งจะกล่าวถึงในภายหลัง
ขั้นตอนที่ 2 สร้างงาน TasksFragmentTest
- เปิด
TasksFragment
- คลิกขวาที่ชื่อชั้นเรียน
TasksFragment
แล้วเลือกสร้าง จากนั้นเลือกทดสอบ สร้างการทดสอบในแหล่งที่มา androidTest - คัดลอกรหัสนี้ไปยัง
TasksFragmentTest
TasksFragmentTest.kt
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}
โค้ดนี้คล้ายกับโค้ด TaskDetailFragmentTest
ที่คุณเขียน ตั้งค่าและทําลาย FakeAndroidTestRepository
ได้ เพิ่มการทดสอบการนําทางเพื่อทดสอบว่าเมื่อคลิกงานในรายการงาน ระบบจะนําคุณไปยัง TaskDetailFragment
ที่ถูกต้อง
- เพิ่มการทดสอบ
clickTask_navigateToDetailFragmentOne
TasksFragmentTest.kt
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
}
- ใช้ฟังก์ชัน Mockito's
mock
เพื่อสร้างการจําลอง
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
หากต้องการจําลองเป็นแบบ Mockito ให้ส่งต่อในชั้นเรียนที่ต้องการจําลอง
ถัดไป คุณต้องเชื่อมโยง NavController
กับส่วนย่อย onFragment
ช่วยให้คุณเรียกใช้เมธอดใน Fragment ได้
- สร้างการจําลองใหม่
NavController
ส่วนย่อย
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- เพิ่มโค้ดเพื่อคลิกรายการใน
RecyclerView
ที่มีข้อความ "TITLE1"
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
RecyclerViewActions
เป็นส่วนหนึ่งของไลบรารี espresso-contrib
และให้คุณทําการดําเนินการของเอสเปรสโซใน RecyclerView
- ยืนยันว่ามีการเรียก
navigate
ด้วยอาร์กิวเมนต์ที่ถูกต้อง
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
วิธีการ verify
ของ Mockito' คือสิ่งที่ทําให้ล้อเลียน โดยคุณสามารถยืนยัน navController
ที่ล้อเลียนที่เรียกว่าเมธอดเฉพาะ (navigate
) ที่มีพารามิเตอร์ (actionTasksFragmentToTaskDetailFragment
ที่มีรหัสของ "id1")
การทดสอบที่สมบูรณ์จะมีลักษณะดังนี้
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}
- ทําการทดสอบ
โดยสรุป หากต้องการทดสอบการนําทาง ให้ทําดังนี้
- ใช้ Mockito เพื่อสร้างการจําลอง
NavController
- แนบส่วนที่ถูกเลียนแบบ
NavController
ไปยังส่วนย่อย - ยืนยันว่าได้เรียกใช้การนําทางด้วยการดําเนินการและพารามิเตอร์ที่ถูกต้อง
ขั้นตอนที่ 3 ไม่บังคับ เขียน clickAddTaskButton_navgateToAddEditFragment
หากต้องการดูว่าคุณสามารถเขียนการทดสอบการนําทางด้วยตัวเองได้หรือไม่ ให้ลองทํางานนี้
- เขียนการทดสอบ
clickAddTaskButton_navigateToAddEditFragment
ซึ่งจะตรวจสอบว่าหากคุณคลิกที่ + FAB จะเป็นการไปที่AddEditTaskFragment
ด้านล่างนี้คือคําตอบ
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}
คลิกที่นี่เพื่อดูความแตกต่างระหว่างโค้ดที่คุณเริ่มและรหัสสุดท้าย
หากต้องการดาวน์โหลดโค้ดสําหรับ Codelab ที่เสร็จสิ้นแล้ว ให้ใช้คําสั่ง git ที่ด้านล่าง
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
คุณอาจดาวน์โหลดที่เก็บเป็นไฟล์ ZIP แล้วแตกไฟล์ และเปิดใน Android Studio ได้ด้วย
Codelab นี้อธิบายวิธีตั้งค่าการแทรกทรัพยากร Dependency ด้วยตนเอง ตัวระบุตําแหน่งบริการ และวิธีปลอมแปลงและจําลองในแอป Android Kotlin โดยเฉพาะอย่างยิ่งฟีเจอร์ต่อไปนี้
- สิ่งที่คุณต้องการทดสอบและกลยุทธ์การทดสอบเป็นตัวกําหนดประเภทของการทดสอบที่คุณจะใช้กับแอป การทดสอบหน่วยจะมุ่งเน้นและรวดเร็ว การทดสอบการผสานรวมจะยืนยันการโต้ตอบระหว่างส่วนต่างๆ ของโปรแกรม การทดสอบแบบปลายทางถึงปลายทางจะยืนยันฟีเจอร์ มีความแม่นยําสูงสุด มักวัดคุม และอาจใช้เวลาดําเนินการนานกว่า
- สถาปัตยกรรมของแอปมีผลต่อความยากต่อการทดสอบ
- TDD หรือ Test Driven Development เป็นกลยุทธ์ที่คุณเขียนการทดสอบก่อน จากนั้นสร้างฟีเจอร์เพื่อผ่านการทดสอบ
- หากต้องการทดสอบส่วนต่างๆ ของแอปเพื่อทําการทดสอบ คุณสามารถใช้การทดสอบแบบคู่ได้ การทดสอบแบบคู่คือเวอร์ชันของชั้นเรียนที่สร้างขึ้นมาเพื่อการทดสอบโดยเฉพาะ ตัวอย่างเช่น คุณปลอมข้อมูลจากฐานข้อมูลหรืออินเทอร์เน็ต
- ใช้การแทรกแบบขึ้นต่อกันเพื่อแทนที่ชั้นเรียนจริงด้วยชั้นเรียนทดสอบ เช่น ที่เก็บหรือเลเยอร์เครือข่าย
- ใช้การทดสอบที่เชื่อถือ (
androidTest
) เพื่อเปิดใช้คอมโพเนนต์ UI - คุณใช้การแทรกตัวระบุบริการไม่ได้ เช่น เมื่อใช้การแทรกทรัพยากร Dependency ของเครื่องมือสร้าง เป็นต้น รูปแบบตัวระบุตําแหน่งบริการเป็นอีกทางเลือกหนึ่งสําหรับใช้การแทรกการอ้างอิง โดยเกี่ยวข้องกับการสร้างคลาสเดี่ยวที่เรียกว่า "Service Locator" โดยมีจุดประสงค์เพื่ออ้างอิงทรัพยากร Dependency ทั้งสําหรับโค้ดปกติและโค้ดทดสอบ
หลักสูตร Udacity:
เอกสารประกอบสําหรับนักพัฒนาซอฟต์แวร์ Android
- คําแนะนําเกี่ยวกับสถาปัตยกรรมแอป
runBlocking
และrunBlockingTest
FragmentScenario
- เอสเปรสโซ
- มิกโตโต
- หน่วยการเรียนรู้ 4
- ไลบรารีการทดสอบของ AndroidX
- ไลบรารีทดสอบหลักของคอมโพเนนต์สถาปัตยกรรม AndroidX
- ชุดแหล่งที่มา
- ทดสอบจากบรรทัดคําสั่ง
วิดีโอ:
อื่นๆ:
สําหรับลิงก์ไปยังหน้า Codelab อื่นๆ ในหลักสูตรนี้ โปรดดูหน้า Landing Page ขั้นสูงสําหรับ Android ใน Kotlin