ข้อมูลเบื้องต้นเกี่ยวกับการทดสอบแบบคู่และการแทรกแบบขึ้นต่อกัน

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

บทนำ

Codelab การทดสอบที่ 2 นี้เป็นการทดสอบแบบ 2 เท่า คือกรณีที่ควรใช้ใน Android และวิธีติดตั้งโดยใช้การแทรกทรัพยากร Dependency, รูปแบบตัวระบุตําแหน่งบริการ และไลบรารี ในการเรียนรู้นี้ คุณจะได้เรียนรู้วิธีเขียนดังนี้

  • การทดสอบหน่วยที่เก็บ
  • ส่วนย่อยและการทดสอบการผสานรวมโมเดล
  • การทดสอบการนําทางที่เป็น Fragment

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

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

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

  • วิธีวางแผนกลยุทธ์การทดสอบ
  • วิธีสร้างและการใช้การทดสอบแบบคู่ คือปลอมและจําลอง
  • วิธีใช้การแทรกทรัพยากร Dependency ด้วยตนเองใน Android สําหรับการทดสอบหน่วยและการผสานรวม
  • วิธีใช้รูปแบบตัวระบุบริการ
  • วิธีทดสอบที่เก็บ ส่วนย่อย ดูโมเดล และคอมโพเนนต์การนําทาง

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

สิ่งที่คุณจะทํา

  • เขียนการทดสอบหน่วยสําหรับที่เก็บโดยใช้การทดสอบแบบ 2 เท่าและการแทรกการขึ้นต่อกัน
  • เขียนการทดสอบหน่วยโฆษณาสําหรับโมเดลข้อมูลพร็อพเพอร์ตี้โดยใช้การทดสอบแบบ 2 เท่าและแทรกการขึ้นต่อกัน
  • เขียนการทดสอบการผสานรวมสําหรับส่วนย่อยและโมเดลข้อมูลพร็อพเพอร์ตี้โดยใช้เฟรมเวิร์กการทดสอบ UI ของ Espresso
  • เขียนการทดสอบการนําทางโดยใช้ Mockito และ Espresso

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

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

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

ด้านล่างนี้เป็นข้อมูลสรุปแพ็กเกจที่คุณจะพบ

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

.addedittask

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

.data

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

.statistics

หน้าจอสถิติ: รหัสเลเยอร์ UI สําหรับหน้าจอสถิติ

.taskdetail

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

.tasks

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

.util

ชั้นเรียนยูทิลิตี้:ชั้นเรียนที่แชร์และใช้ในส่วนต่างๆ ของแอป เช่น สําหรับเลย์เอาต์การรีเฟรชแบบปัดที่ใช้หลายหน้าจอ

ชั้นข้อมูล (.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 รายการร่วมกัน

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

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

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

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



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

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

เราจะกล่าวถึงการทดสอบตั้งแต่ต้นจนจบในบทเรียนถัดไป

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

การทดสอบเฉพาะโค้ดในชั้นเรียนหนึ่งหรือบางคลาสอาจเป็นเรื่องยาก ยกตัวอย่างเช่น เปิดคลาส 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) ซึ่งรวมถึงโค้ดที่เก็บมากกว่าเพียงโค้ดที่เก็บ

เหตุผลที่เจาะจงมากขึ้นสาเหตุที่การทดสอบที่เก็บนั้นเป็นเรื่องยากมีดังนี้

  • คุณต้องคิดเกี่ยวกับการสร้างและจัดการฐานข้อมูลเพื่อทําการทดสอบที่เรียบง่ายที่สุดสําหรับที่เก็บนี้ การทําเช่นนี้จะนําไปสู่คําถามต่างๆ เช่น &quot และควรจะเป็นการทดสอบหรือไม่ใส่รายการทดสอบในท้องถิ่น และควรใช้ AndroidX Test เพื่อสร้างสภาพแวดล้อมจําลองของ Android หรือไม่
  • โค้ดบางส่วน เช่น โค้ดเครือข่าย อาจต้องใช้เวลาทํางานนานๆ หรือบางครั้งอาจทํางานล้มเหลว ทําให้เกิดการทดสอบที่ไม่น่าเชื่อถือเป็นเวลานาน
  • การทดสอบอาจสูญเสียความสามารถในการวิเคราะห์โค้ดที่ผิดพลาดของการทดสอบ การทดสอบอาจเริ่มทดสอบโค้ดที่ไม่ใช่ที่เก็บ ตัวอย่างเช่น ตัวอย่างเช่น การทดสอบหน่วย "repository" อาจไม่สําเร็จเนื่องจากปัญหาในโค้ดบางส่วน เช่น โค้ดฐานข้อมูล

คู่ทดสอบ

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

การทดสอบประเภทสองมีดังนี้

ปลอม

การทดสอบแบบคู่ที่มี "การทํางาน&โควต้า; การใช้งานในชั้นเรียน แต่มีการใช้ในลักษณะที่ได้ผลดีสําหรับการทดสอบแต่ไม่เหมาะสําหรับการผลิต

จําลอง

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

Stub

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

จําลอง

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

สปาย

การทดสอบอีก 1 ครั้งจะเก็บการติดตามข้อมูลเพิ่มเติมไว้ด้วย เช่น หากคุณสร้าง SpyTaskRepository ระบบอาจติดตามจํานวนครั้งที่มีการเรียกเมธอด addTask ด้วย

ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบคู่ได้ที่การทดสอบบนโถสุขภัณฑ์: รู้เท่าทันการทดสอบ

การทดสอบแบบคู่ที่ใช้ใน Android มากที่สุด ได้แก่ Fakes และ Mocks

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

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

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

  1. ในซอร์สโค้ดการทดสอบ ให้คลิกขวาที่ New -> Package

  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 คือการทดสอบบางประเภทที่เฉพาะเจาะจงซึ่งเรียกว่าปลอม ปลอมคือการทดสอบซ้ําที่มีการใช้ & & quot;การทํางาน & quot; ในคลาส แต่มีการใช้ในแนวทางง่ายๆ ที่เหมาะสําหรับการทดสอบ แต่ไม่เหมาะสําหรับการผลิต "Working" การใช้งานหมายความว่าชั้นเรียนจะให้ผลลัพธ์จริงตามอินพุตที่กําหนดไว้

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

FakeDataSource

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


นี่คือรายการงานที่ "fakes" เป็นฐานข้อมูลหรือการตอบสนองของเซิร์ฟเวอร์ ในตอนนี้ คุณต้องทดสอบเมธอด repository'sgetTasks ซึ่งเรียกว่าแหล่งข้อมูล 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 มีการขึ้นต่อกันของ 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

  1. เปลี่ยนเครื่องมือสร้าง DefaultTaskRepository&#39 จากการลงชื่อเข้าใช้ 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 }
  1. เนื่องจากคุณนําทรัพยากร Dependency เข้ามาแล้ว จึงนําเมธอด 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 ในการทดสอบ

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

  1. คลิกขวาที่ชื่อชั้นเรียน DefaultTasksRepository แล้วเลือกสร้าง จากนั้นเลือกทดสอบ
  2. ทําตามข้อความที่ปรากฏเพื่อสร้าง DefaultTasksRepositoryTest ในชุดแหล่งที่มา test
  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 รายการ (ตัวแปร 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: เขียนการทดสอบ getTasks() ของ DefaultTasksRepository

ได้เวลาเขียนบททดสอบภาษา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: เพิ่ม RunBlockTest

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

  1. เพิ่มทรัพยากร 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 ชุดถัดไปได้

  1. เพิ่ม @ExperimentalCoroutinesApi เหนือชั้นเรียน ข้อผิดพลาดนี้แสดงว่าคุณใช้ API Coroutine (runBlockingTest) ในการทดสอบในชั้นเรียน หากไม่มี ก็จะได้รับคําเตือน
  2. กลับไปยัง 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))
    }

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

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

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

ดังนั้น 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

  1. เปิด DefaultTasksRepository และคลิกขวาที่ชื่อชั้นเรียน จากนั้นเลือก Rector -> แยก -> Interface

  1. เลือกแตกไฟล์เพื่อแยกไฟล์

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


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

และ DefaultTasksRepository ใช้งาน TasksRepository แล้ว

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

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

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

  1. ในแหล่งที่มาของชุดแหล่งที่มา ในข้อมูล/แหล่งที่มา ให้สร้างไฟล์ Kotlin และคลาส FakeTestRepository.kt และขยายจากอินเทอร์เฟซ 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 เพิ่มวิธีทดสอบลงใน addTasks

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

  1. เพิ่มเมธอด 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&#39

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

class TasksFragment : Fragment() {

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

}


ดังที่โค้ดข้างต้นใช้การมอบสิทธิ์พร็อพเพอร์ตี้ viewModel's ของโมเดลการสร้างรูปแบบ หากต้องการเปลี่ยนวิธีสร้างรูปแบบมุมมอง คุณต้องเพิ่มและใช้ 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 
}

เนื่องจากคุณเปลี่ยนเครื่องมือสร้าง จึงต้องใช้โรงงานเพื่อสร้าง TasksViewModel ใส่ชั้นเรียนเริ่มต้นในไฟล์เดียวกับ 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 ตอนนี้คุณมีโรงงานแล้ว คุณสามารถใช้เป็นค่าเริ่มต้นเมื่อคุณสร้างโมเดลข้อมูลพร็อพเพอร์ตี้

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

TasksFragment.kt

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

// WITH

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

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

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

  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 เพื่อสร้าง 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 ApplicationProvider.getApplicationContext แล้ว คุณจึงนําคําอธิบายประกอบ @RunWith(AndroidJUnit4::class) ออกได้ด้วย
  2. ทําการทดสอบให้แน่ใจว่าการทดสอบยังทํางานอยู่

การใช้การแทรกทรัพยากร Dependency เครื่องมือสร้างหมายความว่าคุณได้นํา DefaultTasksRepository ที่เป็นทรัพยากร Dependency ออก แล้วแทนที่ด้วย FakeTestRepository ในการทดสอบ

ขั้นตอนที่ 3 อัปเดต TaskDetail Fragment และ Viewmodel ด้วย

ทําการเปลี่ยนแปลงที่เหมือนกันทุกประการสําหรับ 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. ที่ด้านล่างของไฟล์ 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)
}
  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 ได้แล้ว

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

  • รูปแบบ ServiceLocator
  • ห้องสมุด Espresso และ Mockito

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

ในกรณีของคุณ คุณจะได้ทดสอบส่วนย่อยและเขียนการผสานรวมส่วนย่อยสําหรับโมเดลส่วนย่อยและการดูเพื่อทดสอบฟีเจอร์หลักของส่วนย่อย

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

  1. เพิ่มทรัพยากร 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 - ไลบรารีการทดสอบ Coroutine
  • androidx.fragment:fragment-testing - ไลบรารีการทดสอบ AndroidX สําหรับสร้างส่วนย่อยในการทดสอบและเปลี่ยนสถานะ

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

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

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

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

  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 เปิดส่วนย่อยจากการทดสอบ

ในงานนี้ คุณจะต้องเปิดตัว TaskDetailFragment โดยใช้ไลบรารีการทดสอบ AndroidX FragmentScenario เป็นคลาสจากการทดสอบ AndroidX ที่รวมส่วนย่อยและทําให้คุณควบคุมอายุการใช้งานของส่วนย่อยและการทดสอบได้โดยตรง หากต้องการเขียนการทดสอบสําหรับส่วนย่อย คุณจะต้องสร้าง FragmentScenario สําหรับส่วนย่อยที่คุณกําลังทดสอบ (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 ซึ่งแสดงถึงอาร์กิวเมนต์ส่วนย่อยสําหรับงานที่ส่งผ่านไปยังส่วนย่อย
  • ฟังก์ชัน launchFragmentInContainer จะสร้าง FragmentScenario ที่มีแพ็กเกจและธีมนี้

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

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

สิ่งที่ควรดําเนินการ

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

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

การทดสอบทั้ง 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 นี้ ให้ทําดังนี้

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

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

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

หมายเหตุ: ServiceLocator เป็นรายการเดียว คุณจึงควรใช้คีย์เวิร์ด Kotlin object สําหรับชั้นเรียน

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

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

object ServiceLocator {

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

}

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

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

  1. provideTasksRepository - ให้ที่เก็บที่มีอยู่หรือสร้างใหม่ เมธอดนี้ควรเป็น synchronized ใน this เพื่อหลีกเลี่ยงสถานการณ์ที่มีชุดข้อความหลายชุด และเผลอสร้างอินสแตนซ์ที่เก็บข้อมูล 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

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

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

ตอนนี้ไม่ว่าคุณจะใช้ getRepository ที่ไหน ให้ใช้แอปพลิเคชัน taskRepository# แทน จึงทําให้แทนที่คุณจะได้รับที่เก็บโดยตรงใน 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 เพื่อเปลี่ยนการทดสอบเป็น 2 เท่าเมื่อทดสอบ โดยต้องเพิ่มโค้ดลงในโค้ด ServiceLocator

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

ServiceLocator.kt

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

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

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

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

ServiceLocator.kt

private val lock = Any()
  1. เพิ่มเมธอดเฉพาะการทดสอบที่เรียกว่า 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

  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 ในที่เก็บก่อนเปิดใช้ส่วนย่อย
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()

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


ในขั้นตอนนี้ คุณจะใช้ไลบรารีการทดสอบ 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 แนวทางปฏิบัติที่ดีที่สุดในการปิดภาพเคลื่อนไหว (คือการทดสอบจะเร็วขึ้นด้วย):

  1. ในอุปกรณ์ทดสอบ ให้ไปที่การตั้งค่า &gt ตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์
  2. ปิดใช้การตั้งค่า 3 อย่างนี้ ได้แก่ ขนาดภาพเคลื่อนไหวของหน้าต่าง ขนาดภาพเคลื่อนไหวการเปลี่ยน และขนาดระยะเวลาของภาพเคลื่อนไหว

ขั้นตอนที่ 3 ดูการทดสอบเอสเปรสโซ

ก่อนที่จะเขียนการทดสอบเอสเปรสโซ โปรดดูรหัสเอสเปรสโซ

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

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

ข้อความส่วนใหญ่ของเอสเปรสโซประกอบด้วย 4 ส่วน ได้แก่

1. วิธี Espresso แบบคงที่

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 คือสิ่งที่มุมมองทําได้ เช่น การคลิกที่นี่จะคลิกมุมมอง

4. ViewAssertion

check(matches(isChecked()))

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

โปรดทราบว่าคุณไม่จําเป็นต้องเรียกทั้ง perform และ check ในคําสั่งเอสเปรสโซเสมอไป คุณสามารถข้อความที่เพียงแค่ยืนยันโดยใช้ 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 จะใช้เอสเปรสโซ ตรวจสอบโครงสร้างการทดสอบและการใช้งาน withId และตรวจสอบเพื่อยืนยันความถูกต้องของหน้ารายละเอียด
  2. ทําการทดสอบและยืนยันว่าผ่าน

ขั้นตอนที่ 4 (ไม่บังคับ) เขียนการทดสอบเอสเปรสโซของคุณเอง

ให้เขียนการทดสอบเอง

  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 นี้ คุณได้ใช้การทดสอบที่เรียกว่า "ปลอม" 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

  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 - การขึ้นต่อกันแบบ 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. ใช้ฟังก์ชัน Mockito's mock เพื่อสร้างการจําลอง

TasksFragmentTest.kt

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

หากต้องการจําลองเป็นแบบ Mockito ให้ส่งต่อในชั้นเรียนที่ต้องการจําลอง

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

  1. สร้างการจําลองใหม่ NavController ส่วนย่อย
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 และให้คุณทําการดําเนินการของเอสเปรสโซใน RecyclerView

  1. ยืนยันว่ามีการเรียก navigate ด้วยอาร์กิวเมนต์ที่ถูกต้อง
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

วิธีการ verify ของ Mockito&#39 คือสิ่งที่ทําให้ล้อเลียน โดยคุณสามารถยืนยัน 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 ไปยังส่วนย่อย
  3. ยืนยันว่าได้เรียกใช้การนําทางด้วยการดําเนินการและพารามิเตอร์ที่ถูกต้อง

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

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

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

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

หากต้องการดาวน์โหลดโค้ดสําหรับ Codelab ที่เสร็จสิ้นแล้ว ให้ใช้คําสั่ง git ที่ด้านล่าง

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


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

ดาวน์โหลด Zip

Codelab นี้อธิบายวิธีตั้งค่าการแทรกทรัพยากร Dependency ด้วยตนเอง ตัวระบุตําแหน่งบริการ และวิธีปลอมแปลงและจําลองในแอป Android Kotlin โดยเฉพาะอย่างยิ่งฟีเจอร์ต่อไปนี้

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

หลักสูตร Udacity:

เอกสารประกอบสําหรับนักพัฒนาซอฟต์แวร์ Android

วิดีโอ:

อื่นๆ:

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