ข้อมูลเบื้องต้นเกี่ยวกับ Test Doubles และการแทรกการอ้างอิง

Codelab นี้เป็นส่วนหนึ่งของหลักสูตร Android ขั้นสูงใน Kotlin คุณจะได้รับประโยชน์สูงสุดจากหลักสูตรนี้หากทำตาม Codelab ตามลำดับ แต่ไม่จำเป็นต้องทำ Codelab ของหลักสูตรทั้งหมดแสดงอยู่ในหน้า Landing Page ของ Codelab Android ขั้นสูงใน Kotlin

บทนำ

Codelab การทดสอบที่ 2 นี้จะเน้นเรื่องการทดสอบแบบแทน: เมื่อใดควรใช้ใน Android และวิธีใช้การทดสอบแบบแทนโดยใช้การแทรกการอ้างอิง รูปแบบ Service Locator และไลบรารี การทำเช่นนี้จะช่วยให้คุณได้เรียนรู้วิธีเขียนสิ่งต่อไปนี้

  • การทดสอบ 1 หน่วยของที่เก็บ
  • การทดสอบการผสานรวม Fragment และ ViewModel
  • การทดสอบการนำทาง Fragment

สิ่งที่คุณควรทราบอยู่แล้ว

คุณควรคุ้นเคยกับสิ่งต่อไปนี้

สิ่งที่คุณจะได้เรียนรู้

  • วิธีวางแผนกลยุทธ์การทดสอบ
  • วิธีสร้างและใช้การทดสอบแบบคู่ ซึ่งได้แก่ การทดสอบแบบจำลองและการทดสอบแบบจำลอง
  • วิธีใช้การแทรกทรัพยากร Dependency ด้วยตนเองใน Android สำหรับการทดสอบหน่วยและการทดสอบการผสานรวม
  • วิธีใช้รูปแบบ Service Locator
  • วิธีทดสอบที่เก็บข้อมูล Fragment, ViewModel และคอมโพเนนต์การนำทาง

คุณจะได้ใช้ไลบรารีและแนวคิดเกี่ยวกับโค้ดต่อไปนี้

สิ่งที่คุณต้องดำเนินการ

  • เขียนการทดสอบหน่วยสำหรับที่เก็บโดยใช้การทดสอบแบบจำลองและการแทรกการอ้างอิง
  • เขียนการทดสอบหน่วยสำหรับ ViewModel โดยใช้ Test Double และการแทรกการอ้างอิง
  • เขียนการทดสอบการผสานรวมสำหรับ Fragment และ View Model โดยใช้เฟรมเวิร์กการทดสอบ UI ของ Espresso
  • เขียนการทดสอบการนำทางโดยใช้ Mockito และ Espresso

ในชุด Codelab นี้ คุณจะได้ทำงานกับแอปบันทึกสิ่งที่ต้องทำ ซึ่งช่วยให้คุณเขียนงานที่ต้องทำและแสดงงานเหล่านั้นในรายการได้ จากนั้นคุณจะทำเครื่องหมายว่าเสร็จแล้วหรือไม่ กรอง หรือลบรายการก็ได้

แอปนี้เขียนด้วย Kotlin มีหน้าจอ 2-3 หน้าจอ ใช้คอมโพเนนต์ Jetpack และใช้สถาปัตยกรรมจากคำแนะนำเกี่ยวกับสถาปัตยกรรมของแอป การเรียนรู้วิธีทดสอบแอปนี้จะช่วยให้คุณทดสอบแอปที่ใช้ไลบรารีและสถาปัตยกรรมเดียวกันได้

ดาวน์โหลดโค้ด

หากต้องการเริ่มต้นใช้งาน ให้ดาวน์โหลดโค้ดโดยทำดังนี้

ดาวน์โหลด Zip

หรือจะโคลนที่เก็บ 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 ยอดนิยม (ใช้ตัวอย่างเวอร์ชันสถาปัตยกรรมแบบรีแอ็กทีฟ) แอปใช้สถาปัตยกรรมจากคู่มือสถาปัตยกรรมแอป โดยใช้ ViewModel กับ Fragment, ที่เก็บ และ Room หากคุณคุ้นเคยกับตัวอย่างใดตัวอย่างหนึ่งด้านล่าง แสดงว่าแอปนี้มีสถาปัตยกรรมที่คล้ายกัน

คุณควรทำความเข้าใจสถาปัตยกรรมทั่วไปของแอปมากกว่าที่จะทำความเข้าใจตรรกะในเลเยอร์ใดเลเยอร์หนึ่งอย่างลึกซึ้ง

ต่อไปนี้คือสรุปแพ็กเกจที่คุณจะเห็น

แพ็กเกจ: com.example.android.architecture.blueprints.todoapp

.addedittask

หน้าจอเพิ่มหรือแก้ไขงาน: โค้ดเลเยอร์ UI สำหรับเพิ่มหรือแก้ไขงาน

.data

ชั้นข้อมูล: ส่วนนี้จัดการกับชั้นข้อมูลของงาน ซึ่งมีโค้ดฐานข้อมูล เครือข่าย และที่เก็บ

.statistics

หน้าจอสถิติ: โค้ดเลเยอร์ UI สำหรับหน้าจอสถิติ

.taskdetail

หน้าจอรายละเอียดงาน: โค้ดเลเยอร์ UI สำหรับงานเดียว

.tasks

หน้าจอ Tasks: โค้ดเลเยอร์ UI สำหรับรายการงานทั้งหมด

.util

คลาสยูทิลิตี: คลาสที่ใช้ร่วมกันซึ่งใช้ในส่วนต่างๆ ของแอป เช่น สำหรับเลย์เอาต์การปัดเพื่อรีเฟรชที่ใช้ในหลายหน้าจอ

ชั้นข้อมูล (.data)

แอปนี้มีเลเยอร์เครือข่ายจำลองในแพ็กเกจ remote และเลเยอร์ฐานข้อมูลในแพ็กเกจ local เพื่อความสะดวก ในโปรเจ็กต์นี้เราจะจำลองเลเยอร์เครือข่ายด้วย HashMap ที่มีความล่าช้าแทนที่จะสร้างคำขอเครือข่ายจริง

DefaultTasksRepository จะประสานงานหรือเป็นตัวกลางระหว่างเลเยอร์เครือข่ายกับเลเยอร์ฐานข้อมูล และเป็นสิ่งที่ส่งคืนข้อมูลไปยังเลเยอร์ UI

เลเยอร์ UI ( .addedittask, .statistics, .taskdetail, .tasks)

แพ็กเกจเลเยอร์ UI แต่ละแพ็กเกจจะมี Fragment และ ViewModel พร้อมกับคลาสอื่นๆ ที่จำเป็นสำหรับ UI (เช่น อะแดปเตอร์สำหรับรายการงาน) TaskActivity คือกิจกรรมที่มี Fragment ทั้งหมด

การไปยังส่วนต่างๆ

คอมโพเนนต์การนำทางจะควบคุมการนำทางของแอป โดยกำหนดไว้ในไฟล์ nav_graph.xml การนำทางจะทริกเกอร์ใน ViewModel โดยใช้คลาส Event และ ViewModel จะกำหนดอาร์กิวเมนต์ที่จะส่งด้วย Fragment จะสังเกต Event และทำการไปยังส่วนต่างๆ ระหว่างหน้าจอ

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีทดสอบที่เก็บ, โมเดลมุมมอง และ Fragment โดยใช้การทดสอบแบบแทนและ Dependency Injection ก่อนที่จะเจาะลึกว่าการทดสอบเหล่านั้นคืออะไร คุณควรทำความเข้าใจเหตุผลที่จะเป็นแนวทางในการเขียนการทดสอบเหล่านี้

ส่วนนี้จะครอบคลุมแนวทางปฏิบัติแนะนำบางส่วนในการทดสอบโดยทั่วไป ซึ่งใช้กับ Android ได้

พีระมิดการทดสอบ

เมื่อพิจารณากลยุทธ์การทดสอบ มีแง่มุมการทดสอบที่เกี่ยวข้อง 3 ประการดังนี้

  • ขอบเขต - การทดสอบครอบคลุมโค้ดมากน้อยเพียงใด การทดสอบสามารถเรียกใช้ในเมธอดเดียว ในทั้งแอปพลิเคชัน หรือที่ใดก็ได้ระหว่างนั้น
  • ความเร็ว - การทดสอบทำงานเร็วแค่ไหน ความเร็วในการทดสอบอาจแตกต่างกันตั้งแต่ระดับมิลลิวินาทีไปจนถึงหลายนาที
  • ความสมจริง - การทดสอบมีความ "สมจริง" มากน้อยเพียงใด ตัวอย่างเช่น หากส่วนของโค้ดที่คุณกำลังทดสอบต้องส่งคำขอเครือข่าย โค้ดทดสอบจะส่งคำขอเครือข่ายนี้จริงหรือจำลองผลลัพธ์ หากการทดสอบพูดคุยกับเครือข่ายจริง แสดงว่าการทดสอบมีความเที่ยงตรงสูงกว่า ข้อเสียคือการทดสอบอาจใช้เวลานานขึ้น อาจทำให้เกิดข้อผิดพลาดหากเครือข่ายล่ม หรืออาจมีค่าใช้จ่ายสูง

ซึ่งด้านต่างๆ เหล่านี้มีข้อดีและข้อเสียในตัว ตัวอย่างเช่น ความเร็วและความเที่ยงตรงเป็นสิ่งที่ต้องแลกกัน โดยทั่วไปแล้วยิ่งทดสอบเร็ว ความเที่ยงตรงก็จะยิ่งน้อยลง และในทางกลับกัน วิธีทั่วไปในการแบ่งการทดสอบอัตโนมัติคือการแบ่งออกเป็น 3 หมวดหมู่ต่อไปนี้

  • การทดสอบหน่วย - เป็นการทดสอบที่มุ่งเน้นอย่างมากซึ่งทำงานในคลาสเดียว โดยปกติจะเป็นเมธอดเดียวในคลาสนั้น หากการทดสอบหน่วยไม่สำเร็จ คุณจะทราบได้อย่างแน่ชัดว่าปัญหาอยู่ในส่วนใดของโค้ด การทดสอบหน่วยมีความเที่ยงตรงต่ำเนื่องจากในโลกแห่งความเป็นจริง แอปของคุณเกี่ยวข้องกับสิ่งต่างๆ มากกว่าการเรียกใช้เมธอดหรือคลาสเดียว ซึ่งจะทำงานได้เร็วพอที่จะรันทุกครั้งที่คุณเปลี่ยนโค้ด โดยส่วนใหญ่แล้วจะเป็นการทดสอบที่ดำเนินการในเครื่อง (ในtestชุดแหล่งที่มา) ตัวอย่าง: การทดสอบเมธอดเดียวในโมเดลมุมมองและที่เก็บข้อมูล
  • การทดสอบการผสานรวม - การทดสอบนี้จะทดสอบการโต้ตอบของคลาสหลายๆ คลาสเพื่อให้แน่ใจว่าคลาสเหล่านั้นทำงานได้ตามที่คาดไว้เมื่อใช้ร่วมกัน วิธีหนึ่งในการจัดโครงสร้างการทดสอบการผสานรวมคือการทดสอบฟีเจอร์เดียว เช่น ความสามารถในการบันทึกงาน การทดสอบเหล่านี้จะทดสอบโค้ดในขอบเขตที่กว้างกว่าการทำ Unit Test แต่ยังคงได้รับการเพิ่มประสิทธิภาพให้ทำงานได้อย่างรวดเร็วแทนที่จะมีความเที่ยงตรงเต็มรูปแบบ คุณสามารถเรียกใช้การทดสอบเหล่านี้ได้ทั้งในเครื่องหรือเป็นการทดสอบการใช้เครื่องมือ ทั้งนี้ขึ้นอยู่กับสถานการณ์ ตัวอย่าง: การทดสอบฟังก์ชันการทำงานทั้งหมดของคู่ Fragment และ ViewModel รายการเดียว
  • การทดสอบแบบครบวงจร (E2e) - ทดสอบการทำงานร่วมกันของฟีเจอร์ต่างๆ การทดสอบเหล่านี้จะทดสอบแอปในส่วนใหญ่ จำลองการใช้งานจริงอย่างใกล้ชิด และจึงมักจะช้า โดยมีการแสดงผลที่แม่นยำที่สุดและบอกให้คุณทราบว่าแอปพลิเคชันของคุณทำงานได้จริงโดยรวม โดยส่วนใหญ่แล้ว การทดสอบเหล่านี้จะเป็นการทดสอบที่มีการวัดผล (ในandroidTestชุดแหล่งที่มา)
    ตัวอย่าง: การเริ่มต้นแอปทั้งแอปและการทดสอบฟีเจอร์ 2-3 รายการพร้อมกัน

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

สถาปัตยกรรมและการทดสอบ

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

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



ในบทเรียนนี้ คุณจะได้ดูวิธีทดสอบส่วนต่างๆ ของสถาปัตยกรรมข้างต้นโดยแยกส่วนอย่างเหมาะสม

  1. ก่อนอื่นคุณจะต้องทดสอบหน่วยที่เก็บ
  2. จากนั้นคุณจะใช้การทดสอบคู่ใน ViewModel ซึ่งจำเป็นสำหรับการทดสอบหน่วยและการทดสอบการผสานรวม ViewModel
  3. จากนั้น คุณจะได้เรียนรู้วิธีเขียนการทดสอบการผสานรวมสำหรับ Fragment และ View Model
  4. สุดท้าย คุณจะได้เรียนรู้วิธีเขียนการทดสอบการผสานรวมที่มีคอมโพเนนต์การนำทาง

เราจะพูดถึงการทดสอบแบบครบวงจรในบทเรียนถัดไป

เมื่อเขียนการทดสอบหน่วยสำหรับส่วนหนึ่งของคลาส (เมธอดหรือชุดเมธอดขนาดเล็ก) เป้าหมายของคุณคือทดสอบเฉพาะโค้ดในคลาสนั้น

การทดสอบเฉพาะโค้ดในชั้นเรียนที่เฉพาะเจาะจงอาจทำได้ยาก มาดูตัวอย่างกัน เปิดdata.source.DefaultTaskRepositoryชั้นเรียนในmainชุดแหล่งข้อมูล นี่คือที่เก็บสำหรับแอป และเป็นคลาสที่คุณจะเขียนการทดสอบหน่วยในครั้งถัดไป

เป้าหมายของคุณคือการทดสอบเฉพาะโค้ดในคลาสนั้น แต่ DefaultTaskRepository ต้องอาศัยคลาสอื่นๆ เช่น LocalTaskDataSource และ RemoteTaskDataSource ในการทำงาน อีกวิธีหนึ่งในการอธิบายเรื่องนี้คือ LocalTaskDataSource และ RemoteTaskDataSource เป็น Dependency ของ 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 จำลองหรือไม่
  • โค้ดบางส่วน เช่น โค้ดเครือข่าย อาจใช้เวลานานในการเรียกใช้ หรืออาจล้มเหลวในบางครั้ง ซึ่งทำให้เกิดการทดสอบที่ทำงานนานและไม่เสถียร
  • การทดสอบอาจสูญเสียความสามารถในการวินิจฉัยว่าโค้ดใดเป็นสาเหตุที่ทำให้การทดสอบล้มเหลว การทดสอบอาจเริ่มทดสอบโค้ดที่ไม่ใช่ที่เก็บ ดังนั้นตัวอย่างเช่น การทดสอบหน่วย "ที่เก็บ" ที่คุณควรจะทำได้อาจล้มเหลวเนื่องจากปัญหาในโค้ดที่ขึ้นอยู่กับโค้ดบางส่วน เช่น โค้ดฐานข้อมูล

การทดสอบแบบจำลอง

วิธีแก้ปัญหานี้คือเมื่อทดสอบที่เก็บ อย่าใช้โค้ดเครือข่ายหรือฐานข้อมูลจริง แต่ให้ใช้การทดสอบแบบคู่แทน การทดสอบแบบคู่คือเวอร์ชันของคลาสที่สร้างขึ้นเพื่อการทดสอบโดยเฉพาะ โดยมีไว้เพื่อแทนที่เวอร์ชันจริงของคลาสในการทดสอบ ซึ่งคล้ายกับวิธีที่สตั๊นท์แมนเป็นนักแสดงที่เชี่ยวชาญด้านสตั๊นท์และแทนที่นักแสดงจริงสำหรับการกระทำที่เป็นอันตราย

ตัวอย่างประเภทของ Test Double มีดังนี้

ปลอม

การทดสอบแบบคู่ที่มีการติดตั้งใช้งาน "ที่ใช้งานได้" ของคลาส แต่มีการติดตั้งใช้งานในลักษณะที่เหมาะสำหรับการทดสอบ แต่ไม่เหมาะสำหรับการใช้งานจริง

Mock

การทดสอบแบบแทนที่ที่ติดตามว่ามีการเรียกใช้เมธอดใด จากนั้นจะผ่านหรือไม่ผ่านการทดสอบขึ้นอยู่กับว่ามีการเรียกใช้เมธอดอย่างถูกต้องหรือไม่

Stub

การทดสอบแบบจำลองที่ไม่มีตรรกะและแสดงผลเฉพาะสิ่งที่คุณตั้งโปรแกรมให้แสดงผล คุณสามารถตั้งโปรแกรม StubTaskRepository ให้แสดงชุดค่าผสมของงานจาก getTasks ได้ เช่น

Dummy

การทดสอบแบบคู่ที่ส่งต่อแต่ไม่ได้ใช้ เช่น ในกรณีที่คุณเพียงแค่ต้องระบุเป็นพารามิเตอร์ หากคุณมี NoOpTaskRepository ก็จะติดตั้งใช้งาน TaskRepository โดยไม่มีโค้ดในวิธีการใดๆ

Spy

การทดสอบแบบคู่ซึ่งติดตามข้อมูลเพิ่มเติมบางอย่างด้วย เช่น หากคุณสร้าง SpyTaskRepository ก็อาจติดตามจำนวนครั้งที่มีการเรียกใช้เมธอด addTask

ดูข้อมูลเพิ่มเติมเกี่ยวกับเทสต์ดับเบิลได้ที่Testing on the Toilet: Know Your Test Doubles

Test Double ที่ใช้กันมากที่สุดใน Android คือ Fake และ Mock

ในงานนี้ คุณจะได้สร้างFakeDataSourceออบเจ็กต์ทดสอบเพื่อทดสอบหน่วยDefaultTasksRepositoryที่แยกออกจากแหล่งข้อมูลจริง

ขั้นตอนที่ 1: สร้างคลาส FakeDataSource

ในขั้นตอนนี้ คุณจะได้สร้างคลาสชื่อ FakeDataSouce ซึ่งจะเป็นการทดสอบแบบคู่ของ LocalDataSource และ RemoteDataSource

  1. ในชุดแหล่งข้อมูลทดสอบ ให้คลิกขวาแล้วเลือกใหม่ -> แพ็กเกจ

  1. สร้างแพ็กเกจข้อมูลที่มีแพ็กเกจแหล่งที่มาอยู่ภายใน
  2. สร้างคลาสใหม่ชื่อ FakeDataSource ในแพ็กเกจ data/source

ขั้นตอนที่ 2: ใช้อินเทอร์เฟซ TasksDataSource

หากต้องการใช้คลาสใหม่ FakeDataSource เป็นการทดสอบแบบแทนที่ได้ คลาสใหม่ต้องแทนที่แหล่งข้อมูลอื่นๆ ได้ แหล่งข้อมูลดังกล่าวคือ TasksLocalDataSource และ TasksRemoteDataSource

  1. โปรดสังเกตว่าทั้ง 2 อย่างนี้ใช้TasksDataSourceอินเทอร์เฟซอย่างไร
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. วิธีใช้ FakeDataSource เพื่อติดตั้งใช้งาน TasksDataSource
class FakeDataSource : TasksDataSource {

}

Android Studio จะแจ้งว่าคุณยังไม่ได้ใช้เมธอดที่จำเป็นสำหรับ TasksDataSource

  1. ใช้เมนูแก้ไขด่วนแล้วเลือกใช้สมาชิก


  1. เลือกวิธีการทั้งหมด แล้วกดตกลง

ขั้นตอนที่ 3: ใช้เมธอด getTasks ใน FakeDataSource

FakeDataSourceเป็นประเภทเฉพาะของ Test Double ที่เรียกว่า Fake ออบเจ็กต์จำลองคือออบเจ็กต์ทดสอบที่ใช้งานได้จริงของคลาส แต่มีการนำไปใช้ในลักษณะที่เหมาะกับการทดสอบแต่ไม่เหมาะกับการใช้งานจริง การใช้งานที่ "ใช้งานได้" หมายความว่าคลาสจะสร้างเอาต์พุตที่สมจริงเมื่อได้รับอินพุต

เช่น แหล่งข้อมูลจำลองจะไม่เชื่อมต่อกับเครือข่ายหรือบันทึกสิ่งใดลงในฐานข้อมูล แต่จะใช้เพียงรายการในหน่วยความจำ ซึ่งจะ "ทำงานตามที่คุณอาจคาดหวัง" ในแง่ที่ว่าเมธอดในการรับหรือบันทึกงานจะแสดงผลลัพธ์ที่คาดไว้ แต่คุณจะใช้การติดตั้งใช้งานนี้ในเวอร์ชันที่ใช้งานจริงไม่ได้ เนื่องจากระบบไม่ได้บันทึกไว้ในเซิร์ฟเวอร์หรือฐานข้อมูล

A FakeDataSource

  • ช่วยให้คุณทดสอบโค้ดใน DefaultTasksRepository ได้โดยไม่ต้องอาศัยฐานข้อมูลหรือเครือข่ายจริง
  • มีการติดตั้งใช้งานที่ "สมจริง" เพียงพอสำหรับการทดสอบ
  1. เปลี่ยนตัวสร้าง FakeDataSource เพื่อสร้าง var ชื่อ tasks ซึ่งเป็น MutableList<Task>? ที่มีค่าเริ่มต้นเป็นรายการที่เปลี่ยนแปลงได้ที่ว่างเปล่า
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


นี่คือรายการงานที่ "แสร้ง" เป็นการตอบกลับของฐานข้อมูลหรือเซิร์ฟเวอร์ ตอนนี้เป้าหมายคือการทดสอบวิธีการของที่เก็บ getTasks ซึ่งจะเรียกใช้เมธอด getTasks, deleteAllTasks และ saveTask ของแหล่งข้อมูล

เขียนเวอร์ชันปลอมของวิธีการต่อไปนี้

  1. เขียน getTasks: หาก tasks ไม่ใช่ null ให้แสดงผลลัพธ์ Success หาก tasks เป็น null ให้แสดงผลลัพธ์ Error
  2. เขียน deleteAllTasks: ล้างรายการงานที่แก้ไขได้
  3. เขียน 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 เป็นทรัพยากร Dependency ของ 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 การระบุการอ้างอิงทำได้หลายวิธี จึงทำให้มีการแทรกการอ้างอิงหลายประเภท

การแทรกการอ้างอิงของตัวสร้างช่วยให้คุณสลับการทดสอบคู่ได้โดยส่งไปยังตัวสร้าง

ไม่มีการแทรก

การแทรก

ขั้นตอนที่ 1: ใช้การแทรกการอ้างอิงของตัวสร้างใน DefaultTasksRepository

  1. เปลี่ยนตัวสร้างของ DefaultTaskRepository จากการรับ Application เป็นการรับทั้งแหล่งข้อมูลและตัวจัดสรรงานของโครูทีน (ซึ่งคุณจะต้องสลับสำหรับการทดสอบด้วย - อธิบายรายละเอียดเพิ่มเติมในส่วนบทเรียนที่ 3 เกี่ยวกับโครูทีน)

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 }
  1. เนื่องจากคุณส่งผ่านการอ้างอิงเข้ามา ให้นำเมธอด init ออก คุณไม่จำเป็นต้องสร้างทรัพยากร Dependency อีกต่อไป
  2. และลบตัวแปรอินสแตนซ์เก่าด้วย คุณกําลังกําหนดค่าในเครื่องมือสร้าง

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. สุดท้าย ให้อัปเดตเมธอด 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 ในการทดสอบ

ตอนนี้โค้ดใช้การแทรกทรัพยากร Dependency ของตัวสร้างแล้ว คุณจึงใช้แหล่งข้อมูลจำลองเพื่อทดสอบ DefaultTasksRepository ได้

  1. คลิกขวาที่DefaultTasksRepositoryชื่อคลาส แล้วเลือกสร้าง จากนั้นเลือกทดสอบ
  2. ทำตามข้อความแจ้งเพื่อสร้าง DefaultTasksRepositoryTest ในชุดแหล่งข้อมูลการทดสอบ
  3. ที่ด้านบนของคลาส 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 }
  1. สร้างตัวแปร 3 ตัว ได้แก่ ตัวแปรสมาชิก FakeDataSource 2 ตัว (ตัวแปรละ 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

  1. สร้างเมธอดชื่อ createRepository แล้วใส่คำอธิบายประกอบด้วย @Before
  2. สร้างอินสแตนซ์แหล่งข้อมูลจำลองโดยใช้รายการ remoteTasks และ localTasks
  3. สร้างอินสแตนซ์ 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: เขียนการทดสอบ DefaultTasksRepository getTasks()

ได้เวลาเขียนDefaultTasksRepositoryข้อสอบแล้ว

  1. เขียนการทดสอบสำหรับเมธอด 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: เพิ่ม runBlockingTest

ข้อผิดพลาดของโครูทีนเป็นสิ่งที่คาดไว้เนื่องจาก getTasks เป็นฟังก์ชัน suspend และคุณต้องเปิดใช้โครูทีนเพื่อเรียกใช้ ซึ่งคุณต้องมีขอบเขตของโครูทีน หากต้องการแก้ไขข้อผิดพลาดนี้ คุณจะต้องเพิ่มการอ้างอิง Gradle บางอย่างเพื่อจัดการการเปิดใช้โครูทีนในการทดสอบ

  1. เพิ่มการอ้างอิงที่จำเป็นสำหรับการทดสอบโครูทีนไปยังชุดแหล่งที่มาของการทดสอบโดยใช้ testImplementation

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

อย่าลืมซิงค์

kotlinx-coroutines-test คือไลบรารีทดสอบโครูทีนที่สร้างขึ้นเพื่อทดสอบโครูทีนโดยเฉพาะ หากต้องการเรียกใช้การทดสอบ ให้ใช้ฟังก์ชัน runBlockingTest นี่คือฟังก์ชันที่จัดทำโดยไลบรารีการทดสอบโครูทีน โดยจะรับบล็อกโค้ด จากนั้นเรียกใช้บล็อกโค้ดนี้ในบริบทของโครูทีนพิเศษซึ่งทำงานพร้อมกันและทันที ซึ่งหมายความว่าการดำเนินการจะเกิดขึ้นตามลำดับที่แน่นอน ซึ่งจะทำให้โครูทีนทำงานเหมือนกับโค้ดที่ไม่ใช่โครูทีน ดังนั้นจึงมีไว้สำหรับการทดสอบโค้ด

ใช้ runBlockingTest ในคลาสทดสอบเมื่อเรียกใช้ฟังก์ชัน suspend คุณจะได้ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีที่ runBlockingTest ทำงานและวิธีทดสอบโครูทีนใน Codelab ถัดไปในชุดนี้

  1. เพิ่ม @ExperimentalCoroutinesApi เหนือชั้นเรียน ซึ่งแสดงให้เห็นว่าคุณทราบว่ากำลังใช้ API ของโครูทีนเวอร์ชันทดลอง (runBlockingTest) ในคลาส หากไม่มี คุณจะได้รับคำเตือน
  2. กลับไปที่ DefaultTasksRepositoryTest แล้วเพิ่ม runBlockingTest เพื่อให้ระบบนำการทดสอบทั้งหมดของคุณมาใช้เป็น "บล็อก" ของโค้ด

การทดสอบขั้นสุดท้ายนี้มีลักษณะเหมือนโค้ดด้านล่าง

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))
    }

}
  1. เรียกใช้getTasks_requestsAllTasksFromRemoteDataSourceการทดสอบใหม่และยืนยันว่าการทดสอบทำงานได้และไม่มีข้อผิดพลาด

คุณเพิ่งเห็นวิธีทดสอบหน่วยของที่เก็บ ในขั้นตอนถัดไปนี้ คุณจะต้องใช้การแทรกการอ้างอิงอีกครั้งและสร้างการทดสอบแบบคู่ขึ้นมาอีกครั้ง คราวนี้เพื่อแสดงวิธีเขียนการทดสอบหน่วยและการทดสอบการผสานรวมสำหรับ ViewModel

การทดสอบหน่วยควรทดสอบเฉพาะคลาสหรือเมธอดที่คุณสนใจเท่านั้น ซึ่งเรียกว่าการทดสอบในไอโซเลชัน ซึ่งคุณจะแยก "หน่วย" ออกอย่างชัดเจนและทดสอบเฉพาะโค้ดที่เป็นส่วนหนึ่งของหน่วยนั้น

ดังนั้น TasksViewModelTest ควรทดสอบเฉพาะโค้ด TasksViewModel เท่านั้น ไม่ควรทดสอบในฐานข้อมูล เครือข่าย หรือคลาสที่เก็บ ดังนั้นสำหรับ ViewModel คุณจะต้องสร้างที่เก็บข้อมูลจำลองและใช้การขึ้นต่อกันเพื่อใช้ในเทสต์ เช่นเดียวกับที่เพิ่งทำกับที่เก็บข้อมูล

ในงานนี้ คุณจะใช้การแทรกการอ้างอิงกับ ViewModel

ขั้นตอนที่ 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

  1. เปิด DefaultTasksRepository แล้วคลิกขวาที่ชื่อชั้นเรียน จากนั้นเลือกปรับโครงสร้าง -> แยก -> อินเทอร์เฟซ

  1. เลือกแยกเป็นไฟล์แยก

  1. ในหน้าต่างอินเทอร์เฟซการแยก ให้เปลี่ยนชื่ออินเทอร์เฟซเป็น TasksRepository
  2. ในส่วนอินเทอร์เฟซสมาชิกที่จะสร้าง ให้เลือกสมาชิกทั้งหมดยกเว้นสมาชิกคู่ 2 คนและวิธีการส่วนตัว


  1. คลิกปรับโครงสร้าง TasksRepositoryอินเทอร์เฟซใหม่ควรปรากฏในแพ็กเกจ data/source

และDefaultTasksRepositoryตอนนี้ได้นำTasksRepositoryมาใช้แล้ว

  1. เรียกใช้แอป (ไม่ใช่การทดสอบ) เพื่อให้แน่ใจว่าทุกอย่างยังคงทำงานได้

ขั้นตอนที่ 2 สร้าง FakeTestRepository

ตอนนี้คุณมีอินเทอร์เฟซแล้ว คุณก็สร้างDefaultTaskRepositoryเทสต์ดับเบิลได้

  1. ในชุดแหล่งข้อมูลทดสอบ ให้สร้างไฟล์และคลาส Kotlin FakeTestRepository.kt ใน data/source และขยายจากอินเทอร์เฟซ TasksRepository

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

ระบบจะแจ้งให้คุณทราบว่าต้องใช้วิธีการของอินเทอร์เฟซ

  1. วางเมาส์เหนือข้อผิดพลาดจนกว่าจะเห็นเมนูคำแนะนำ จากนั้นคลิกและเลือกใช้สมาชิก
  1. เลือกวิธีการทั้งหมด แล้วกดตกลง

ขั้นตอนที่ 3 ใช้เมธอด FakeTestRepository

ตอนนี้คุณมีคลาส FakeTestRepository ที่มีเมธอด "ไม่ได้ใช้" แล้ว FakeTestRepository จะได้รับการสนับสนุนโดยโครงสร้างข้อมูลแทนที่จะต้องจัดการกับการไกล่เกลี่ยที่ซับซ้อนระหว่างแหล่งข้อมูลในเครื่องและแหล่งข้อมูลระยะไกล ซึ่งคล้ายกับวิธีที่คุณใช้ FakeDataSource

โปรดทราบว่าFakeTestRepositoryไม่จำเป็นต้องใช้FakeDataSourceหรืออะไรที่คล้ายกัน เพียงแค่ต้องแสดงผลลวงที่สมจริงเมื่อได้รับอินพุต คุณจะใช้ LinkedHashMap เพื่อจัดเก็บรายการงานและ MutableLiveData สำหรับงานที่สังเกตได้

  1. ใน FakeTestRepository ให้เพิ่มทั้งตัวแปร LinkedHashMap ที่แสดงรายการงานปัจจุบันและ MutableLiveData สำหรับงานที่สังเกตได้

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

ติดตั้งใช้งานวิธีต่อไปนี้

  1. getTasks - วิธีนี้ควรใช้ tasksServiceData และเปลี่ยนเป็นรายการโดยใช้ tasksServiceData.values.toList() จากนั้นส่งคืนเป็นผลลัพธ์ Success
  2. refreshTasks - อัปเดตค่าของ observableTasks ให้เป็นค่าที่ getTasks() แสดงผล
  3. 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 เพิ่มวิธีการทดสอบเพื่อเพิ่มงาน

เมื่อทดสอบ คุณควรมีTasksอยู่ในที่เก็บอยู่แล้ว คุณอาจเรียกใช้ saveTask หลายครั้ง แต่เพื่อความสะดวก ให้เพิ่มเมธอดตัวช่วยสําหรับการทดสอบโดยเฉพาะซึ่งช่วยให้คุณเพิ่มงานได้

  1. เพิ่มaddTasksเมธอด ซึ่งรับvarargของงาน เพิ่มแต่ละงานลงใน HashMap แล้วรีเฟรชงาน

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

ตอนนี้คุณมีที่เก็บข้อมูลจำลองสำหรับการทดสอบโดยใช้วิธีการหลักๆ บางอย่างแล้ว จากนั้นใช้ฟีเจอร์นี้ในการทดสอบ

ในงานนี้ คุณจะได้ใช้ชั้นเรียนจำลองภายใน ViewModel ใช้การแทรกการอ้างอิงของตัวสร้างเพื่อรับแหล่งข้อมูล 2 แหล่งผ่านการแทรกการอ้างอิงของตัวสร้างโดยการเพิ่มตัวแปร TasksRepository ลงในตัวสร้างของ TasksViewModel

กระบวนการนี้จะแตกต่างจากโมเดลมุมมองเล็กน้อยเนื่องจากคุณไม่ได้สร้างโมเดลมุมมองโดยตรง เช่น

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


เช่นเดียวกับในโค้ดด้านบน คุณกำลังใช้viewModel's property delegate ซึ่งสร้าง ViewModel หากต้องการเปลี่ยนวิธีสร้าง View Model คุณจะต้องเพิ่มและใช้ ViewModelProvider.Factory หากคุณไม่คุ้นเคยกับ ViewModelProvider.Factory โปรดดูข้อมูลเพิ่มเติมที่นี่

ขั้นตอนที่ 1 สร้างและใช้ ViewModelFactory ใน TasksViewModel

คุณเริ่มต้นด้วยการอัปเดตชั้นเรียนและการทดสอบที่เกี่ยวข้องกับTasksหน้าจอ

  1. เปิด TasksViewModel
  2. เปลี่ยนตัวสร้างของ 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 
}

เนื่องจากคุณเปลี่ยนตัวสร้างแล้ว ตอนนี้คุณต้องใช้ Factory เพื่อสร้าง TasksViewModel ใส่คลาส Factory ในไฟล์เดียวกับ TasksViewModel แต่คุณจะใส่ไว้ในไฟล์ของตัวเองก็ได้

  1. ที่ด้านล่างของไฟล์ 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 ตอนนี้คุณมี Factory แล้ว ให้ใช้ Factory นี้ทุกที่ที่คุณสร้าง ViewModel

  1. อัปเดต TasksFragment เพื่อใช้โรงงาน

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. เรียกใช้โค้ดแอปและตรวจสอบว่าทุกอย่างยังคงทำงานได้

ขั้นตอนที่ 2 ใช้ FakeTestRepository ภายใน TasksViewModelTest

ตอนนี้คุณสามารถใช้ที่เก็บข้อมูลจำลองแทนที่เก็บข้อมูลจริงในการทดสอบ ViewModel ได้แล้ว

  1. เปิดTasksViewModelTest
  2. เพิ่มพร็อพเพอร์ตี้ 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
}
  1. อัปเดตsetupViewModel method เพื่อสร้าง 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)
        
    }
  1. เนื่องจากคุณไม่ได้ใช้โค้ด AndroidX Test ApplicationProvider.getApplicationContext อีกต่อไป คุณจึงนำคำอธิบายประกอบ @RunWith(AndroidJUnit4::class) ออกได้ด้วย
  2. เรียกใช้การทดสอบและตรวจสอบว่าการทดสอบทั้งหมดยังคงทำงานได้

การใช้การแทรกการอ้างอิงของตัวสร้างทำให้คุณนำ DefaultTasksRepository ออกจากการอ้างอิงและแทนที่ด้วย FakeTestRepository ในการทดสอบได้แล้ว

ขั้นตอนที่ 3 นอกจากนี้ ให้อัปเดต Fragment และ ViewModel ของ TaskDetail ด้วย

ทำการเปลี่ยนแปลงเดียวกันทุกประการสำหรับ TaskDetailFragment และ TaskDetailViewModel ซึ่งจะเตรียมโค้ดไว้สำหรับเมื่อคุณเขียนการทดสอบ TaskDetail ในครั้งถัดไป

  1. เปิด TaskDetailViewModel
  2. อัปเดตตัวสร้างโดยทำดังนี้

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 }
  1. เพิ่ม TaskDetailViewModelFactory ที่ด้านล่างของTaskDetailViewModel นอกคลาส

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)
}
  1. อัปเดต TasksFragment เพื่อใช้โรงงาน

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. เรียกใช้โค้ดและตรวจสอบว่าทุกอย่างทำงานได้ตามปกติ

ตอนนี้คุณใช้ FakeTestRepository แทนที่รีพอสิทอรีจริงใน TasksFragment และ TasksDetailFragment ได้แล้ว

จากนั้นคุณจะเขียนการทดสอบการผสานรวมเพื่อทดสอบการโต้ตอบของ Fragment และ ViewModel คุณจะทราบว่าโค้ด ViewModel อัปเดต UI อย่างเหมาะสมหรือไม่ โดยใช้

  • รูปแบบ ServiceLocator
  • ไลบรารี Espresso และ Mockito

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

ในกรณีของคุณ คุณจะต้องใช้แต่ละ Fragment และเขียนการทดสอบการผสานรวมสำหรับ Fragment และ ViewModel เพื่อทดสอบฟีเจอร์หลักของ Fragment

ขั้นตอนที่ 1 เพิ่มการขึ้นต่อกันของ Gradle

  1. เพิ่มการขึ้นต่อกันของ 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"

ซึ่งการขึ้นต่อกันเหล่านี้ ได้แก่

  • junit:junit—JUnit ซึ่งจำเป็นสำหรับการเขียนคำสั่งทดสอบพื้นฐาน
  • androidx.test:core—ไลบรารีการทดสอบหลักของ AndroidX
  • kotlinx-coroutines-test - ไลบรารีการทดสอบโครูทีน
  • androidx.fragment:fragment-testing - ไลบรารีการทดสอบ AndroidX สำหรับสร้าง Fragment ในการทดสอบและเปลี่ยนสถานะของ Fragment

เนื่องจากคุณจะใช้ไลบรารีเหล่านี้ในandroidTestชุดแหล่งที่มา ให้ใช้ androidTestImplementation เพื่อเพิ่มไลบรารีเป็นทรัพยากร Dependency

ขั้นตอนที่ 2 สร้างคลาส TaskDetailFragmentTest

TaskDetailFragment แสดงข้อมูลเกี่ยวกับงานเดียว

คุณจะเริ่มต้นด้วยการเขียนการทดสอบ Fragment สำหรับ TaskDetailFragment เนื่องจากมีฟังก์ชันพื้นฐานพอสมควรเมื่อเทียบกับ Fragment อื่นๆ

  1. เปิด taskdetail.TaskDetailFragment
  2. สร้างการทดสอบสำหรับ TaskDetailFragment เหมือนที่เคยทำ ยอมรับตัวเลือกเริ่มต้นและวางไว้ในชุดแหล่งที่มา androidTest (ไม่ใช่ชุดแหล่งที่มา test)

  1. เพิ่มคำอธิบายประกอบต่อไปนี้ในคลาส TaskDetailFragmentTest

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

วัตถุประสงค์ของคำอธิบายประกอบเหล่านี้มีดังนี้

  • @MediumTest - ทำเครื่องหมายการทดสอบเป็นการทดสอบการผสานรวม "รันไทม์ปานกลาง" (เทียบกับการทดสอบหน่วย @SmallTest และการทดสอบแบบครบวงจรขนาดใหญ่ @LargeTest) ซึ่งจะช่วยให้คุณจัดกลุ่มและเลือกขนาดของการทดสอบที่จะเรียกใช้ได้
  • @RunWith(AndroidJUnit4::class) - ใช้ในคลาสที่ใช้ AndroidX Test

ขั้นตอนที่ 3 เปิดใช้ Fragment จากการทดสอบ

ในงานนี้ คุณจะได้เปิดใช้ TaskDetailFragment โดยใช้ไลบรารีการทดสอบ AndroidX FragmentScenario เป็นคลาสจาก AndroidX Test ที่ครอบคลุม Fragment และให้คุณควบคุมวงจรของ Fragment ได้โดยตรงเพื่อการทดสอบ หากต้องการเขียนการทดสอบสำหรับ Fragment ให้สร้าง FragmentScenario สำหรับ Fragment ที่คุณกำลังทดสอบ (TaskDetailFragment)

  1. คัดลอกการทดสอบนี้ลงใน 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 ซึ่งแสดงอาร์กิวเมนต์ของ Fragment สำหรับงานที่ส่งไปยัง Fragment
  • ฟังก์ชัน launchFragmentInContainer จะสร้าง FragmentScenario พร้อมกับแพ็กเกจและธีมนี้

นี่ไม่ใช่การทดสอบที่เสร็จสมบูรณ์เนื่องจากไม่ได้ยืนยันอะไรเลย ตอนนี้ ให้เรียกใช้การทดสอบและสังเกตสิ่งที่เกิดขึ้น

  1. นี่คือการทดสอบที่มีการตรวจสอบ ดังนั้นโปรดตรวจสอบว่าโปรแกรมจำลองหรืออุปกรณ์ของคุณมองเห็นได้
  2. ทำการทดสอบ

ระบบควรดำเนินการต่อไปนี้

  • ประการแรก เนื่องจากเป็นการทดสอบที่มีการวัดผล การทดสอบจะทำงานบนอุปกรณ์จริง (หากเชื่อมต่อ) หรือโปรแกรมจำลอง
  • ซึ่งควรเปิด Fragment
  • โปรดสังเกตว่า Fragment นี้ไม่ได้ไปยัง Fragment อื่นหรือมีเมนูใดๆ ที่เชื่อมโยงกับกิจกรรม แต่เป็นเพียง Fragment เท่านั้น

สุดท้าย ให้สังเกตว่าส่วนย่อยแสดงข้อความ "ไม่มีข้อมูล" เนื่องจากโหลดข้อมูลงานไม่สำเร็จ

การทดสอบทั้ง 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 แต่ต้องมีวิธีแทนที่ที่เก็บข้อมูลจริงด้วยที่เก็บข้อมูลปลอมสำหรับ Fragment คุณจะทำสิ่งนี้ในขั้นตอนถัดไป

ในงานนี้ คุณจะต้องระบุที่เก็บข้อมูลปลอมให้กับ Fragment โดยใช้ ServiceLocator ซึ่งจะช่วยให้คุณเขียนการทดสอบการผสานรวม Fragment และ View Model ได้

คุณไม่สามารถใช้การแทรกการอ้างอิงของตัวสร้างที่นี่ได้เหมือนที่เคยทำเมื่อต้องระบุการอ้างอิงไปยัง ViewModel หรือที่เก็บ การแทรกการอ้างอิงของ Constructor กำหนดให้คุณต้องสร้างคลาส Fragment และ Activity เป็นตัวอย่างของคลาสที่คุณไม่ได้สร้างและโดยทั่วไปจะไม่มีสิทธิ์เข้าถึงตัวสร้าง

เนื่องจากคุณไม่ได้สร้าง Fragment จึงใช้การแทรกทรัพยากร Dependency ของตัวสร้างเพื่อสลับการทดสอบแบบจำลองของที่เก็บ (FakeTestRepository) ไปยัง Fragment ไม่ได้ แต่ให้ใช้รูปแบบ Service Locator แทน รูปแบบ Service Locator เป็นอีกทางเลือกหนึ่งแทนการแทรก Dependency ซึ่งเกี่ยวข้องกับการสร้างคลาส Singleton ที่เรียกว่า "Service Locator" ซึ่งมีจุดประสงค์เพื่อจัดหาทรัพยากร Dependency ทั้งสำหรับโค้ดปกติและโค้ดทดสอบ ในโค้ดแอปปกติ (mainชุดแหล่งที่มา) การขึ้นต่อกันทั้งหมดนี้เป็นการขึ้นต่อกันของแอปปกติ สำหรับการทดสอบ คุณจะแก้ไข Service Locator เพื่อระบุการทดสอบเวอร์ชันคู่ของ Dependency

ไม่ได้ใช้เครื่องมือระบุตำแหน่งบริการ


การใช้เครื่องมือค้นหาบริการ

สำหรับแอป Codelab นี้ ให้ทำดังนี้

  1. สร้างคลาส Service Locator ที่สามารถสร้างและจัดเก็บที่เก็บได้ โดยค่าเริ่มต้นจะสร้างที่เก็บ "ปกติ"
  2. ปรับโครงสร้างโค้ดเพื่อให้เมื่อต้องการที่เก็บ ให้ใช้ตัวระบุตำแหน่งบริการ
  3. ในคลาสการทดสอบ ให้เรียกใช้เมธอดใน Service Locator ซึ่งจะสลับที่เก็บ "ปกติ" กับการทดสอบแบบคู่

ขั้นตอนที่ 1 สร้าง ServiceLocator

มาสร้างServiceLocator คลาสกัน โดยจะอยู่ในชุดแหล่งข้อมูลหลักพร้อมกับโค้ดแอปที่เหลือ เนื่องจากโค้ดแอปพลิเคชันหลักใช้โค้ดนี้

หมายเหตุ: ServiceLocator เป็น Singleton ดังนั้นให้ใช้คีย์เวิร์ด object Kotlin สำหรับคลาส

  1. สร้างไฟล์ ServiceLocator.kt ที่ระดับบนสุดของชุดแหล่งข้อมูลหลัก
  2. กำหนด object ชื่อ ServiceLocator
  3. สร้างตัวแปรอินสแตนซ์ database และ repository แล้วตั้งค่าทั้ง 2 รายการเป็น null
  4. ใส่คำอธิบายประกอบที่รีโปซิทอรีด้วย @Volatile เนื่องจากอาจมีการใช้โดยหลายเธรด (@Volatile มีคำอธิบายโดยละเอียดที่นี่)

โค้ดของคุณควรมีลักษณะดังที่แสดงด้านล่าง

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

ในตอนนี้ สิ่งเดียวที่ ServiceLocator ต้องทำคือรู้วิธีส่งคืน TasksRepository โดยจะส่งคืน DefaultTasksRepository ที่มีอยู่แล้ว หรือสร้างและส่งคืน DefaultTasksRepository ใหม่หากจำเป็น

กำหนดฟังก์ชันต่อไปนี้

  1. provideTasksRepository - ระบุที่เก็บที่มีอยู่แล้วหรือสร้างที่เก็บใหม่ วิธีนี้ควรsynchronizedthisเพื่อหลีกเลี่ยงการสร้างอินสแตนซ์ที่เก็บ 2 รายการโดยไม่ตั้งใจในสถานการณ์ที่มีหลายเธรดทำงาน
  2. createTasksRepository - รหัสสำหรับการสร้างที่เก็บใหม่ จะโทรหา createTaskLocalDataSource และสร้าง TasksRemoteDataSource ใหม่
  3. createTaskLocalDataSource - โค้ดสําหรับสร้างแหล่งข้อมูลในเครื่องใหม่ จะโทรหา createDataBase
  4. 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

คุณควรสร้างอินสแตนซ์ของคลาสที่เก็บเพียงอินสแตนซ์เดียวเท่านั้น คุณจะใช้เครื่องมือค้นหาบริการในคลาส Application เพื่อให้มั่นใจว่าจะเป็นเช่นนั้น

  1. ที่ระดับบนสุดของลำดับชั้นแพ็กเกจ ให้เปิด 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 ออกได้

  1. เปิด 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
            }
        }
    }
}

ตอนนี้ให้ใช้ taskRepository ของแอปพลิเคชันแทนทุกที่ที่คุณเคยใช้ getRepository ซึ่งจะช่วยให้คุณได้รับที่เก็บที่ ServiceLocator จัดหาให้แทนที่จะสร้างที่เก็บโดยตรง

  1. เปิด TaskDetailFragement แล้วมองหาการเรียกใช้ getRepository ที่ด้านบนของคลาส
  2. แทนที่การเรียกนี้ด้วยการเรียกที่รับที่เก็บจาก 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)
}
  1. ให้ทำแบบเดียวกันกับ 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)
    }
  1. สำหรับ StatisticsViewModel และ AddEditTaskViewModel ให้อัปเดตโค้ดที่ได้ที่เก็บเพื่อใช้ที่เก็บจาก TodoApplication

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. เรียกใช้แอปพลิเคชัน (ไม่ใช่การทดสอบ)

เนื่องจากคุณรีแฟกเตอร์เท่านั้น แอปจึงควรทำงานได้เหมือนเดิมโดยไม่มีปัญหา

ขั้นตอนที่ 3 สร้าง FakeAndroidTestRepository

คุณมี FakeTestRepository ในชุดแหล่งข้อมูลทดสอบอยู่แล้ว คุณไม่สามารถแชร์คลาสการทดสอบระหว่างชุดแหล่งที่มา test กับ androidTest ได้โดยค่าเริ่มต้น ดังนั้น คุณต้องทำสำเนาคลาส FakeTestRepository ในชุดแหล่งที่มา androidTest และตั้งชื่อว่า FakeAndroidTestRepository

  1. คลิกขวาชุดแหล่งข้อมูล androidTest แล้วสร้างแพ็กเกจข้อมูล คลิกขวาอีกครั้งแล้วสร้างแพ็กเกจแหล่งที่มา
  2. สร้างคลาสใหม่ในแพ็กเกจแหล่งที่มานี้ชื่อ FakeAndroidTestRepository.kt
  3. คัดลอกโค้ดต่อไปนี้ไปยังชั้นเรียนนั้น

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 เพื่อสลับการทดสอบแทนเมื่อทำการทดสอบ โดยคุณต้องเพิ่มโค้ดบางอย่างลงในโค้ด ServiceLocator

  1. เปิด ServiceLocator.kt
  2. ทำเครื่องหมายตัวตั้งค่าสำหรับ tasksRepository เป็น @VisibleForTesting คำอธิบายประกอบนี้เป็นวิธีแสดงให้เห็นว่าเหตุผลที่ตัวตั้งค่าเป็นแบบสาธารณะก็เนื่องมาจากการทดสอบ

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

ไม่ว่าคุณจะทำการทดสอบเพียงอย่างเดียวหรือในการทดสอบกลุ่ม การทดสอบควรทำงานเหมือนกันทุกประการ ซึ่งหมายความว่าการทดสอบไม่ควรมีลักษณะการทำงานที่ขึ้นอยู่กับกันและกัน (ซึ่งหมายความว่าไม่ควรแชร์ออบเจ็กต์ระหว่างการทดสอบ)

เนื่องจาก ServiceLocator เป็น Singleton จึงอาจมีการแชร์ระหว่างการทดสอบโดยไม่ตั้งใจ โปรดสร้างวิธีการที่รีเซ็ตสถานะ ServiceLocator อย่างถูกต้องระหว่างการทดสอบเพื่อช่วยหลีกเลี่ยงปัญหานี้

  1. เพิ่มตัวแปรอินสแตนซ์ที่ชื่อ lock โดยมีค่า Any

ServiceLocator.kt

private val lock = Any()
  1. เพิ่มเมธอดเฉพาะสำหรับการทดสอบที่ชื่อ resetRepository ซึ่งจะล้างฐานข้อมูลและตั้งค่าทั้งที่เก็บและฐานข้อมูลเป็น null

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

  1. เปิด TaskDetailFragmentTest
  2. ประกาศตัวแปร lateinit TasksRepository
  3. เพิ่มวิธีการตั้งค่าและวิธีการล้างข้อมูลเพื่อตั้งค่า FakeAndroidTestRepository ก่อนการทดสอบแต่ละครั้งและล้างข้อมูลหลังการทดสอบแต่ละครั้ง

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. ห่อหุ้มเนื้อหาฟังก์ชันของ activeTaskDetails_DisplayedInUi() ใน runBlockingTest
  2. บันทึก activeTask ในที่เก็บก่อนเปิด Fragment
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)

    }
  1. ใส่คำอธิบายประกอบทั้งชั้นเรียนด้วย @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)

    }

}
  1. ทำการทดสอบ activeTaskDetails_DisplayedInUi()

คุณควรเห็น Fragment เช่นเดียวกับก่อนหน้านี้ แต่คราวนี้เนื่องจากคุณตั้งค่าที่เก็บอย่างถูกต้อง Fragment จึงแสดงข้อมูลงาน


ในขั้นตอนนี้ คุณจะใช้ไลบรารีการทดสอบ UI ของ Espresso เพื่อทำการทดสอบการผสานรวมครั้งแรกให้เสร็จสมบูรณ์ คุณได้จัดโครงสร้างโค้ดเพื่อให้เพิ่มการทดสอบด้วยข้อความยืนยันสำหรับ UI ได้ โดยคุณจะใช้ไลบรารีการทดสอบ Espresso

Espresso ช่วยให้คุณทำสิ่งต่อไปนี้ได้

  • โต้ตอบกับมุมมอง เช่น คลิกปุ่ม เลื่อนแถบ หรือเลื่อนหน้าจอลง
  • ยืนยันว่ามุมมองบางอย่างอยู่บนหน้าจอหรืออยู่ในสถานะหนึ่งๆ (เช่น มีข้อความที่เฉพาะเจาะจง หรือมีการเลือกช่องทำเครื่องหมาย เป็นต้น)

ขั้นตอนที่ 1 หมายเหตุการขึ้นต่อกันของ Gradle

คุณจะมีทรัพยากร Dependency หลักของ Espresso อยู่แล้วเนื่องจากรวมอยู่ในโปรเจ็กต์ 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 จะทำงานบนอุปกรณ์จริง จึงเป็นการทดสอบการใช้เครื่องมือโดยธรรมชาติ ปัญหาหนึ่งที่เกิดขึ้นคือภาพเคลื่อนไหว หากภาพเคลื่อนไหวล่าช้าและคุณพยายามทดสอบว่ามุมมองอยู่บนหน้าจอหรือไม่ แต่ภาพเคลื่อนไหวนั้นยังคงทำงานอยู่ Espresso อาจทำให้การทดสอบล้มเหลวโดยไม่ตั้งใจ ซึ่งอาจทำให้การทดสอบ Espresso ไม่น่าเชื่อถือ

สำหรับการทดสอบ UI ของ Espresso แนวทางปฏิบัติแนะนำคือการปิดภาพเคลื่อนไหว (การทดสอบจะทำงานได้เร็วขึ้นด้วย)

  1. ในอุปกรณ์ทดสอบ ให้ไปที่การตั้งค่า > ตัวเลือกสำหรับนักพัฒนาแอป
  2. ปิดการตั้งค่าทั้ง 3 รายการ ได้แก่ อัตราการเคลื่อนไหวของหน้าต่าง อัตราการเคลื่อนไหวของการเปลี่ยนภาพ และอัตราความเร็วตามตัวสร้างภาพเคลื่อนไหว

ขั้นตอนที่ 3 ดูการทดสอบ Espresso

ก่อนที่จะเขียนการทดสอบ Espresso ให้ดูโค้ด Espresso บางส่วน

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

คำสั่งนี้จะค้นหามุมมองช่องทําเครื่องหมายที่มีรหัส task_detail_complete_checkbox คลิก แล้วยืนยันว่ามีการเลือกช่องทําเครื่องหมาย

ข้อความ Espresso ส่วนใหญ่ประกอบด้วย 4 ส่วนต่อไปนี้

1. วิธี Static Espresso

onView

onView เป็นตัวอย่างของวิธีการ Espresso แบบคงที่ที่เริ่มต้นคำสั่ง Espresso onView เป็นหนึ่งในตัวเลือกที่พบบ่อยที่สุด แต่ก็มีตัวเลือกอื่นๆ เช่น onData

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId เป็นตัวอย่างของ ViewMatcher ซึ่งรับข้อมูลพร็อพเพอร์ตี้ตามรหัส นอกจากนี้ยังมีตัวเทียบมุมมองอื่นๆ ที่คุณดูได้ในเอกสารประกอบ

3. ViewAction

perform(click())

เมธอด perform ซึ่งใช้ ViewAction ViewAction คือสิ่งที่ทำกับมุมมองได้ เช่น ในที่นี้คือการคลิกมุมมอง

4. ViewAssertion

check(matches(isChecked()))

check ซึ่งใช้เวลา ViewAssertion ViewAssertionตรวจสอบหรือยืนยันบางอย่างเกี่ยวกับมุมมอง ViewAssertion ที่พบบ่อยที่สุดที่คุณจะใช้คือการยืนยัน matches หากต้องการจบการยืนยัน ให้ใช้ ViewMatcher อีกอัน ในกรณีนี้คือ isChecked

โปรดทราบว่าคุณไม่จำเป็นต้องเรียกใช้ทั้ง perform และ check ในคำสั่ง Espresso เสมอไป คุณสามารถมีข้อความที่ยืนยันโดยใช้ check หรือเพียงแค่ทำ ViewAction โดยใช้ perform

  1. เปิด TaskDetailFragmentTest.kt
  2. อัปเดตการทดสอบ 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
  1. ทุกอย่างหลังจากความคิดเห็น // THEN จะใช้ Espresso ตรวจสอบโครงสร้างการทดสอบและการใช้ withId และตรวจสอบเพื่อยืนยันว่าหน้ารายละเอียดควรมีลักษณะอย่างไร
  2. เรียกใช้การทดสอบและยืนยันว่าผ่าน

ขั้นตอนที่ 4 ไม่บังคับ: เขียนการทดสอบ Espresso ของคุณเอง

ตอนนี้ก็เขียนการทดสอบด้วยตัวคุณเอง

  1. สร้างการทดสอบใหม่ชื่อ 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
}
  1. ดูการทดสอบก่อนหน้า แล้วทำการทดสอบนี้ให้เสร็จสมบูรณ์
  2. เรียกใช้และยืนยันว่าการทดสอบผ่าน

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 นี้ คุณได้ใช้การทดสอบแบบคู่ที่เรียกว่าการทดสอบแบบจำลอง ออบเจ็กต์จำลองเป็นหนึ่งในออบเจ็กต์ทดสอบจำลองหลายประเภท คุณควรใช้การทดสอบแบบคู่ใดในการทดสอบคอมโพเนนต์การนำทาง

พิจารณาว่าการนำทางเกิดขึ้นได้อย่างไร ลองนึกภาพการกดงานใดงานหนึ่งใน TasksFragment เพื่อไปยังหน้าจอรายละเอียดงาน

นี่คือโค้ดใน TasksFragment ที่ไปยังหน้าจอรายละเอียดงานเมื่อกด

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


การนำทางเกิดขึ้นเนื่องจากการเรียกใช้เมธอด navigate หากคุณต้องการเขียนคำสั่งยืนยัน ก็ไม่มีวิธีที่ตรงไปตรงมาในการทดสอบว่าคุณได้ไปยัง TaskDetailFragment หรือไม่ การไปยังส่วนต่างๆ เป็นการดำเนินการที่ซับซ้อนซึ่งไม่ส่งผลให้เกิดเอาต์พุตที่ชัดเจนหรือการเปลี่ยนแปลงสถานะ นอกเหนือจากการเริ่มต้น TaskDetailFragment

สิ่งที่คุณยืนยันได้คือมีการเรียกใช้เมธอด navigate โดยมีพารามิเตอร์การดำเนินการที่ถูกต้อง ซึ่งเป็นสิ่งที่การจำลองการทดสอบทำ นั่นคือการตรวจสอบว่ามีการเรียกใช้เมธอดที่เฉพาะเจาะจงหรือไม่

Mockito เป็นเฟรมเวิร์กสำหรับการสร้างเทสต์ดับเบิล แม้ว่าคำว่า "จำลอง" จะใช้ใน API และชื่อ แต่ก็ไม่ได้มีไว้สำหรับการสร้างโมเดลจำลองเท่านั้น นอกจากนี้ยังสร้าง Stub และ Spy ได้ด้วย

คุณจะใช้ Mockito เพื่อสร้าง Mock NavigationController ซึ่งสามารถยืนยันได้ว่ามีการเรียกใช้เมธอด navigate อย่างถูกต้อง

ขั้นตอนที่ 1 เพิ่มการขึ้นต่อกันของ Gradle

  1. เพิ่มทรัพยากร 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 - นี่คือทรัพยากร Dependency ของ Mockito
  • dexmaker-mockito - ต้องใช้ไลบรารีนี้เพื่อใช้ Mockito ในโปรเจ็กต์ Android Mockito ต้องสร้างคลาสที่รันไทม์ ใน Android จะดำเนินการนี้โดยใช้ไบต์โค้ด dex ดังนั้นไลบรารีนี้จึงช่วยให้ Mockito สร้างออบเจ็กต์ได้ในระหว่างรันไทม์บน Android
  • androidx.test.espresso:espresso-contrib - ไลบรารีนี้ประกอบด้วยการมีส่วนร่วมภายนอก (จึงเป็นที่มาของชื่อ) ซึ่งมีโค้ดการทดสอบสำหรับข้อมูลพร็อพเพอร์ตี้ขั้นสูง เช่น DatePicker และ RecyclerView นอกจากนี้ ยังมีการตรวจสอบการช่วยเหลือพิเศษและคลาสที่ชื่อ CountingIdlingResource ซึ่งจะกล่าวถึงในภายหลัง

ขั้นตอนที่ 2 สร้าง TasksFragmentTest

  1. เปิด TasksFragment
  2. คลิกขวาที่TasksFragmentชื่อคลาส แล้วเลือกสร้าง จากนั้นเลือกทดสอบ สร้างการทดสอบในชุดแหล่งที่มา androidTest
  3. คัดลอกรหัสนี้ไปยัง 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 ที่ถูกต้อง

  1. เพิ่มการทดสอบ 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)
        
    }
  1. ใช้ฟังก์ชัน mock ของ Mockito เพื่อสร้างการจำลอง

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

หากต้องการจำลองใน Mockito ให้ส่งคลาสที่ต้องการจำลอง

จากนั้นคุณต้องเชื่อมโยง NavController กับ Fragment onFragment ช่วยให้คุณเรียกใช้เมธอดใน Fragment เองได้

  1. สร้างโมเดลจำลองใหม่เป็น NavController ของ Fragment
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. เพิ่มโค้ดเพื่อคลิกรายการใน 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ไลบรารีและช่วยให้คุณทำการดำเนินการ Espresso ใน RecyclerView ได้

  1. ตรวจสอบว่ามีการเรียกใช้ 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")
    )
}
  1. ทำการทดสอบ

โดยสรุปแล้ว หากต้องการทดสอบการนำทาง คุณสามารถทำสิ่งต่อไปนี้ได้

  1. ใช้ Mockito เพื่อสร้างNavControllerจำลอง
  2. แนบ NavController ที่จำลองนั้นกับ Fragment
  3. ตรวจสอบว่ามีการเรียกใช้ navigate ด้วยการดำเนินการและพารามิเตอร์ที่ถูกต้อง

ขั้นตอนที่ 3 ไม่บังคับ ให้เขียน clickAddTaskButton_navigateToAddEditFragment

หากต้องการดูว่าคุณเขียนการทดสอบการนำทางด้วยตัวเองได้หรือไม่ ให้ลองทำตามงานนี้

  1. เขียนการทดสอบ 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)
            )
        )
    }

คลิกที่นี่เพื่อดูความแตกต่างระหว่างโค้ดที่คุณเริ่มต้นกับโค้ดสุดท้าย

หากต้องการดาวน์โหลดโค้ดสำหรับโค้ดแล็บที่เสร็จสมบูรณ์แล้ว คุณสามารถใช้คำสั่ง Git ด้านล่าง

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


หรือคุณจะดาวน์โหลดที่เก็บเป็นไฟล์ Zip, แตกไฟล์ และเปิดใน Android Studio ก็ได้

ดาวน์โหลด Zip

ในโค้ดแล็บนี้ เราได้พูดถึงวิธีตั้งค่าการแทรกทรัพยากร Dependency ด้วยตนเอง, Service Locator และวิธีใช้ Fake และ Mock ในแอป Android Kotlin โดยเฉพาะอย่างยิ่งฟีเจอร์ต่อไปนี้

  • สิ่งที่คุณต้องการทดสอบและกลยุทธ์การทดสอบจะเป็นตัวกำหนดประเภทการทดสอบที่คุณจะใช้กับแอป การทดสอบหน่วยจะเน้นและรวดเร็ว การทดสอบการผสานรวมจะยืนยันการโต้ตอบระหว่างส่วนต่างๆ ของโปรแกรม การทดสอบแบบครบวงจรจะยืนยันฟีเจอร์ มีความเที่ยงตรงสูงสุด มักจะมีการวัดผล และอาจใช้เวลานานกว่าในการเรียกใช้
  • สถาปัตยกรรมของแอปมีผลต่อความยากในการทดสอบ
  • TDD หรือการพัฒนาที่ขับเคลื่อนด้วยการทดสอบเป็นกลยุทธ์ที่คุณเขียนการทดสอบก่อน แล้วจึงสร้างฟีเจอร์เพื่อให้ผ่านการทดสอบ
  • หากต้องการแยกส่วนต่างๆ ของแอปเพื่อทดสอบ คุณสามารถใช้การทดสอบแบบคู่ได้ การทดสอบแบบคู่คือเวอร์ชันของคลาสที่สร้างขึ้นเพื่อการทดสอบโดยเฉพาะ เช่น คุณแกล้งทำเป็นรับข้อมูลจากฐานข้อมูลหรืออินเทอร์เน็ต
  • ใช้การแทรกการอ้างอิงเพื่อแทนที่คลาสจริงด้วยคลาสทดสอบ เช่น ที่เก็บข้อมูลหรือเลเยอร์เครือข่าย
  • ใช้การทดสอบที่มีการตรวจสอบ (androidTest) เพื่อเปิดใช้คอมโพเนนต์ UI
  • เมื่อใช้การแทรกทรัพยากร Dependency ของตัวสร้างไม่ได้ เช่น เพื่อเปิด Fragment คุณมักจะใช้ Service Locator ได้ รูปแบบ Service Locator เป็นทางเลือกแทนการแทรกการอ้างอิง ซึ่งเกี่ยวข้องกับการสร้างคลาส Singleton ที่เรียกว่า "Service Locator" ซึ่งมีจุดประสงค์เพื่อจัดหาทรัพยากร Dependency ทั้งสำหรับโค้ดปกติและโค้ดทดสอบ

หลักสูตร Udacity:

เอกสารประกอบสำหรับนักพัฒนาแอป Android

วิดีโอ:

อื่นๆ:

ดูลิงก์ไปยัง Codelab อื่นๆ ในหลักสูตรนี้ได้ที่หน้า Landing Page ของ Codelab Android ขั้นสูงใน Kotlin