Giới thiệu về Kiểm thử kép và Chèn phần phụ thuộc

Lớp học lập trình này nằm trong khoá học Kotlin nâng cao cho Android. Bạn sẽ nhận được nhiều giá trị nhất qua khoá học này nếu thực hiện các lớp học lập trình theo trình tự, nhưng đó không phải là yêu cầu bắt buộc. Tất cả các lớp học lập trình của khoá học đều được liệt kê trên trang đích của lớp học lập trình Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.

Giới thiệu

Lớp học lập trình kiểm thử thứ hai này tập trung vào các đối tượng kiểm thử: thời điểm sử dụng các đối tượng này trong Android và cách triển khai các đối tượng này bằng cách sử dụng tính năng chèn phần phụ thuộc, mẫu Service Locator và các thư viện. Khi làm việc này, bạn sẽ học được cách viết:

  • Kiểm thử đơn vị kho lưu trữ
  • Kiểm thử tích hợp các mảnh và viewmodel
  • Kiểm thử thao tác điều hướng trong phân đoạn

Kiến thức bạn cần có

Bạn cần thông thạo:

Kiến thức bạn sẽ học được

  • Cách lập kế hoạch cho chiến lược kiểm thử
  • Cách tạo và sử dụng đối tượng kiểm thử, cụ thể là đối tượng giả lập và đối tượng mô phỏng
  • Cách sử dụng tính năng chèn phần phụ thuộc theo cách thủ công trên Android cho các bài kiểm thử đơn vị và kiểm thử tích hợp
  • Cách áp dụng Mẫu bộ định vị dịch vụ
  • Cách kiểm thử kho lưu trữ, mảnh, mô hình hiển thị và Thành phần điều hướng

Bạn sẽ sử dụng các thư viện và khái niệm mã sau:

Bạn sẽ thực hiện

  • Viết kiểm thử đơn vị cho một kho lưu trữ bằng cách sử dụng đối tượng kiểm thử thay thế và tính năng chèn phần phụ thuộc.
  • Viết các bài kiểm thử đơn vị cho một mô hình hiển thị bằng cách sử dụng một đối tượng kiểm thử giả và tính năng chèn phần phụ thuộc.
  • Viết bài kiểm thử tích hợp cho các mảnh và mô hình khung hiển thị của chúng bằng khung kiểm thử giao diện người dùng Espresso.
  • Viết các kiểm thử điều hướng bằng Mockito và Espresso.

Trong loạt lớp học lập trình này, bạn sẽ làm việc với ứng dụng TO-DO Notes. Ứng dụng này cho phép bạn viết ra các việc cần làm và hiển thị chúng trong một danh sách. Sau đó, bạn có thể đánh dấu là đã hoàn thành hoặc chưa hoàn thành, lọc hoặc xoá các mục đó.

Ứng dụng này được viết bằng Kotlin, có một số màn hình, sử dụng các thành phần Jetpack và tuân theo cấu trúc trong Hướng dẫn về cấu trúc ứng dụng. Khi tìm hiểu cách kiểm thử ứng dụng này, bạn sẽ có thể kiểm thử các ứng dụng sử dụng cùng một thư viện và cấu trúc.

Tải mã nguồn xuống

Để bắt đầu, hãy tải mã xuống:

Tải tệp Zip xuống

Ngoài ra, bạn còn có thể sao chép kho lưu trữ GitHub cho mã:

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

Hãy dành chút thời gian để làm quen với đoạn mã bằng cách làm theo hướng dẫn bên dưới.

Bước 1: Chạy ứng dụng mẫu

Sau khi tải ứng dụng TO-DO xuống, hãy mở ứng dụng này trong Android Studio và chạy ứng dụng. Thao tác này sẽ biên dịch. Khám phá ứng dụng bằng cách làm như sau:

  • Tạo một việc cần làm mới bằng nút hành động nổi dấu cộng. Trước tiên, hãy nhập tiêu đề, sau đó nhập thông tin bổ sung về việc cần làm. Lưu bằng nút hành động nổi có dấu kiểm màu xanh lục.
  • Trong danh sách việc cần làm, hãy nhấp vào tiêu đề của việc cần làm mà bạn vừa hoàn thành và xem màn hình chi tiết của việc cần làm đó để xem phần còn lại của nội dung mô tả.
  • Trong danh sách hoặc trên màn hình chi tiết, hãy đánh dấu vào hộp đánh dấu của nhiệm vụ đó để đặt trạng thái thành Đã hoàn thành.
  • Quay lại màn hình công việc, mở trình đơn bộ lọc và lọc công việc theo trạng thái Đang hoạt độngĐã hoàn thành.
  • Mở ngăn điều hướng rồi nhấp vào Thống kê.
  • Quay lại màn hình tổng quan, rồi trong trình đơn ngăn điều hướng, hãy chọn Xoá việc đã hoàn thành để xoá tất cả việc cần làm có trạng thái Đã hoàn thành

Bước 2: Khám phá mã ứng dụng mẫu

Ứng dụng TO-DO dựa trên mẫu kiểm thử và cấu trúc Architecture Blueprints phổ biến (sử dụng phiên bản cấu trúc phản ứng của mẫu). Ứng dụng tuân theo cấu trúc trong Hướng dẫn về cấu trúc ứng dụng. Ứng dụng này sử dụng ViewModel với các Mảnh, một kho lưu trữ và Room. Nếu bạn đã quen thuộc với bất kỳ ví dụ nào dưới đây, thì ứng dụng này có cấu trúc tương tự:

Điều quan trọng hơn là bạn phải hiểu được cấu trúc chung của ứng dụng thay vì hiểu sâu về logic ở một lớp bất kỳ.

Sau đây là thông tin tóm tắt về các gói bạn sẽ thấy:

Gói: com.example.android.architecture.blueprints.todoapp

.addedittask

Màn hình thêm hoặc chỉnh sửa việc cần làm: Mã lớp giao diện người dùng để thêm hoặc chỉnh sửa việc cần làm.

.data

Lớp dữ liệu: Lớp này xử lý lớp dữ liệu của các tác vụ. Thư mục này chứa cơ sở dữ liệu, mạng và mã kho lưu trữ.

.statistics

Màn hình số liệu thống kê: Mã lớp giao diện người dùng cho màn hình số liệu thống kê.

.taskdetail

Màn hình thông tin về công việc: Mã lớp giao diện người dùng cho một công việc.

.tasks

Màn hình tác vụ: Mã lớp giao diện người dùng cho danh sách tất cả các tác vụ.

.util

Lớp tiện ích: Các lớp dùng chung được dùng trong nhiều phần của ứng dụng, chẳng hạn như bố cục làm mới bằng thao tác vuốt được dùng trên nhiều màn hình.

Lớp dữ liệu (.data)

Ứng dụng này có một lớp mạng mô phỏng trong gói remote và một lớp cơ sở dữ liệu trong gói local. Để đơn giản, trong dự án này, lớp mạng được mô phỏng chỉ bằng một HashMap có độ trễ, thay vì gửi các yêu cầu mạng thực.

DefaultTasksRepository điều phối hoặc làm trung gian giữa lớp mạng và lớp cơ sở dữ liệu, đồng thời là lớp trả về dữ liệu cho lớp giao diện người dùng.

Lớp giao diện người dùng ( .addedittask, .statistics, .taskdetail, .tasks)

Mỗi gói lớp giao diện người dùng đều chứa một mảnh và một mô hình khung hiển thị, cùng với mọi lớp khác cần thiết cho giao diện người dùng (chẳng hạn như một bộ chuyển đổi cho danh sách việc cần làm). TaskActivity là hoạt động chứa tất cả các mảnh.

Điều hướng

Hoạt động điều hướng cho ứng dụng do thành phần Điều hướng kiểm soát. Giá trị này được xác định trong tệp nav_graph.xml. Hoạt động điều hướng được kích hoạt trong các mô hình hiển thị bằng cách sử dụng lớp Event; các mô hình hiển thị cũng xác định những đối số cần truyền. Các mảnh này theo dõi Event và thực hiện thao tác điều hướng thực tế giữa các màn hình.

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách kiểm thử các kho lưu trữ, mô hình hiển thị và mảnh bằng cách sử dụng các đối tượng kiểm thử và tính năng chèn phần phụ thuộc. Trước khi tìm hiểu về các thử nghiệm đó, bạn cần hiểu rõ lý do sẽ hướng dẫn bạn viết những thử nghiệm này như thế nào và viết về điều gì.

Phần này trình bày một số phương pháp hay nhất về kiểm thử nói chung, vì chúng áp dụng cho Android.

Kim tự tháp kiểm thử

Khi nghĩ về chiến lược kiểm thử, bạn cần xem xét 3 khía cạnh kiểm thử có liên quan:

  • Phạm vi – Bài kiểm thử ảnh hưởng đến bao nhiêu phần của mã? Các kiểm thử có thể chạy trên một phương thức duy nhất, trên toàn bộ ứng dụng hoặc ở đâu đó ở giữa.
  • Tốc độ – Tốc độ chạy kiểm thử là bao nhiêu? Tốc độ kiểm thử có thể dao động từ mili giây đến vài phút.
  • Độ trung thực – Mức độ "thực tế" của kiểm thử? Ví dụ: nếu một phần mã mà bạn đang kiểm thử cần đưa ra yêu cầu mạng, thì mã kiểm thử có thực sự đưa ra yêu cầu mạng này hay không, hoặc mã kiểm thử có giả mạo kết quả hay không? Nếu kiểm thử thực sự giao tiếp với mạng, tức là kiểm thử có độ trung thực cao hơn. Nhược điểm là quá trình kiểm thử có thể mất nhiều thời gian hơn để chạy, có thể dẫn đến lỗi nếu mạng bị gián đoạn hoặc có thể tốn kém khi sử dụng.

Có những điểm đánh đổi vốn có giữa các khía cạnh này. Ví dụ: tốc độ và độ trung thực là một sự đánh đổi – kiểm thử càng nhanh thì độ trung thực thường càng thấp và ngược lại. Một cách phổ biến để chia các kiểm thử tự động thành 3 danh mục sau:

  • Kiểm thử đơn vị – Đây là những bài kiểm thử có độ tập trung cao, chạy trên một lớp duy nhất, thường là một phương thức duy nhất trong lớp đó. Nếu một bài kiểm thử đơn vị không đạt, bạn có thể biết chính xác vấn đề nằm ở đâu trong mã của mình. Các bài kiểm thử này có độ trung thực thấp vì trong thực tế, ứng dụng của bạn liên quan đến nhiều thứ hơn là việc thực thi một phương thức hoặc lớp. Các kiểm thử này đủ nhanh để chạy mỗi khi bạn thay đổi mã. Đây thường là các bài kiểm thử chạy cục bộ (trong nhóm tài nguyên test). Ví dụ: Kiểm thử các phương thức riêng lẻ trong mô hình hiển thị và kho lưu trữ.
  • Kiểm thử tích hợp – Các kiểm thử này kiểm thử hoạt động tương tác của một số lớp để đảm bảo chúng hoạt động như dự kiến khi được dùng cùng nhau. Một cách để cấu trúc các bài kiểm thử tích hợp là để chúng kiểm thử một tính năng duy nhất, chẳng hạn như khả năng lưu một việc cần làm. Các kiểm thử này kiểm thử phạm vi mã rộng hơn so với kiểm thử đơn vị, nhưng vẫn được tối ưu hoá để chạy nhanh, thay vì có độ trung thực hoàn toàn. Bạn có thể chạy các kiểm thử này cục bộ hoặc dưới dạng kiểm thử đo lường, tuỳ thuộc vào tình huống. Ví dụ: Kiểm thử tất cả chức năng của một cặp mảnh và mô hình hiển thị duy nhất.
  • Kiểm thử toàn diện (E2e) – Kiểm thử một tổ hợp các tính năng hoạt động cùng nhau. Các kiểm thử này kiểm thử phần lớn ứng dụng, mô phỏng sát với cách sử dụng thực tế và do đó thường chậm. Các kiểm thử này có độ trung thực cao nhất và cho bạn biết rằng ứng dụng của bạn thực sự hoạt động như một tổng thể. Nhìn chung, những kiểm thử này sẽ là kiểm thử đo lường (trong nhóm tài nguyên androidTest)
    Ví dụ: Khởi động toàn bộ ứng dụng và kiểm thử một vài tính năng cùng nhau.

Tỷ lệ đề xuất của các kiểm thử này thường được biểu thị bằng một kim tự tháp, trong đó phần lớn các kiểm thử là kiểm thử đơn vị.

Cấu trúc và kiểm thử

Khả năng kiểm thử ứng dụng ở tất cả các cấp độ khác nhau của kim tự tháp kiểm thử vốn có liên quan đến cấu trúc của ứng dụng. Ví dụ: một ứng dụng có cấu trúc cực kỳ kém có thể đặt tất cả logic của ứng dụng vào một phương thức. Bạn có thể viết một bài kiểm thử toàn diện cho việc này, vì những bài kiểm thử này thường kiểm thử các phần lớn của ứng dụng, nhưng còn việc viết bài kiểm thử đơn vị hoặc kiểm thử tích hợp thì sao? Khi tất cả mã nằm ở một nơi, bạn khó có thể chỉ kiểm thử mã liên quan đến một đơn vị hoặc tính năng duy nhất.

Cách tiếp cận tốt hơn là chia logic ứng dụng thành nhiều phương thức và lớp, cho phép kiểm thử từng phần riêng biệt. Cấu trúc là cách chia nhỏ và sắp xếp mã của bạn, giúp bạn kiểm thử đơn vị và kiểm thử tích hợp dễ dàng hơn. Ứng dụng việc cần làm mà bạn sẽ kiểm thử tuân theo một cấu trúc cụ thể:



Trong bài học này, bạn sẽ thấy cách kiểm thử các phần của cấu trúc nêu trên một cách riêng biệt:

  1. Trước tiên, bạn sẽ kiểm thử đơn vị kho lưu trữ.
  2. Sau đó, bạn sẽ sử dụng một đối tượng kiểm thử thay thế trong mô hình hiển thị. Đây là đối tượng cần thiết cho việc kiểm thử đơn vịkiểm thử tích hợp mô hình hiển thị.
  3. Tiếp theo, bạn sẽ tìm hiểu cách viết kiểm thử tích hợp cho các mảnh và mô hình hiển thị của chúng.
  4. Cuối cùng, bạn sẽ tìm hiểu cách viết các bài kiểm thử tích hợp bao gồm cả Thành phần điều hướng.

Chúng ta sẽ tìm hiểu về kiểm thử toàn diện trong bài học tiếp theo.

Khi viết một bài kiểm thử đơn vị cho một phần của lớp (một phương thức hoặc một nhóm nhỏ các phương thức), mục tiêu của bạn là chỉ kiểm thử mã trong lớp đó.

Việc chỉ kiểm thử mã trong một hoặc nhiều lớp cụ thể có thể sẽ khó khăn. Hãy xem ví dụ. Mở lớp data.source.DefaultTaskRepository trong tập hợp nguồn main. Đây là kho lưu trữ cho ứng dụng và là lớp mà bạn sẽ viết các bài kiểm thử đơn vị cho bước tiếp theo.

Mục tiêu của bạn là chỉ kiểm thử mã trong lớp đó. Tuy nhiên, DefaultTaskRepository phụ thuộc vào các lớp khác, chẳng hạn như LocalTaskDataSourceRemoteTaskDataSource, để hoạt động. Một cách khác để nói điều này là LocalTaskDataSourceRemoteTaskDataSourcecác phần phụ thuộc của DefaultTaskRepository.

Vì vậy, mọi phương thức trong DefaultTaskRepository đều gọi các phương thức trên các lớp nguồn dữ liệu, đến lượt các phương thức này gọi các phương thức trong các lớp khác để lưu thông tin vào cơ sở dữ liệu hoặc giao tiếp với mạng.



Ví dụ: hãy xem phương thức này trong 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 là một trong những lệnh gọi "cơ bản" nhất mà bạn có thể thực hiện đối với kho lưu trữ của mình. Phương thức này bao gồm việc đọc từ cơ sở dữ liệu SQLite và thực hiện các lệnh gọi mạng (lệnh gọi đến updateTasksFromRemoteDataSource). Điều này liên quan đến nhiều mã hơn chỉ mã kho lưu trữ.

Dưới đây là một số lý do cụ thể hơn khiến việc kiểm thử kho lưu trữ trở nên khó khăn:

  • Bạn cần phải suy nghĩ về việc tạo và quản lý cơ sở dữ liệu để thực hiện ngay cả những kiểm thử đơn giản nhất cho kho lưu trữ này. Điều này đặt ra những câu hỏi như "đây có phải là một kiểm thử cục bộ hay được hỗ trợ?" và liệu bạn có nên sử dụng Thử nghiệm AndroidX để có được một môi trường Android mô phỏng hay không.
  • Một số phần của mã, chẳng hạn như mã mạng, có thể mất nhiều thời gian để chạy hoặc đôi khi thậm chí không thành công, tạo ra các kiểm thử không ổn định và mất nhiều thời gian.
  • Các kiểm thử của bạn có thể mất khả năng chẩn đoán mã nào gây ra lỗi kiểm thử. Các bài kiểm thử của bạn có thể bắt đầu kiểm thử mã không thuộc kho lưu trữ. Ví dụ: các bài kiểm thử đơn vị "kho lưu trữ" mà bạn giả định có thể không thành công do một vấn đề trong một số mã phụ thuộc, chẳng hạn như mã cơ sở dữ liệu.

Đối tượng kiểm thử giả

Giải pháp cho vấn đề này là khi bạn đang kiểm thử kho lưu trữ, đừng sử dụng mã mạng hoặc cơ sở dữ liệu thực mà hãy sử dụng một bản sao kiểm thử. Kiểm thử kép là một phiên bản của lớp được tạo riêng cho mục đích kiểm thử. Mục đích của lớp này là thay thế phiên bản thực của một lớp trong các bài kiểm thử. Tương tự như cách diễn viên đóng thế là một diễn viên chuyên đóng các cảnh mạo hiểm và thay thế diễn viên thật trong các cảnh hành động nguy hiểm.

Sau đây là một số loại đối tượng kiểm thử:

Giả mạo

Một đối tượng kiểm thử có quá trình triển khai "đang hoạt động" của lớp, nhưng được triển khai theo cách phù hợp với các kiểm thử nhưng không phù hợp với phiên bản phát hành công khai.

Mock

Một đối tượng kiểm thử theo dõi phương thức nào đã được gọi. Sau đó, nó sẽ vượt qua hoặc không vượt qua một bài kiểm thử tuỳ thuộc vào việc các phương thức của nó có được gọi đúng cách hay không.

Stub

Một đối tượng kiểm thử không chứa logic và chỉ trả về những gì bạn lập trình để trả về. StubTaskRepository có thể được lập trình để trả về một số tổ hợp tác vụ nhất định từ getTasks, chẳng hạn.

Dummy

Một đối tượng kiểm thử được truyền xung quanh nhưng không được sử dụng, chẳng hạn như nếu bạn chỉ cần cung cấp đối tượng đó dưới dạng một tham số. Nếu bạn có NoOpTaskRepository, thì bạn chỉ cần triển khai TaskRepositorykhông có mã trong bất kỳ phương thức nào.

Spy

Một kiểm thử kép cũng theo dõi một số thông tin bổ sung; ví dụ: nếu bạn tạo một SpyTaskRepository, thì kiểm thử đó có thể theo dõi số lần phương thức addTask được gọi.

Để biết thêm thông tin về đối tượng kiểm thử, hãy xem bài viết Kiểm thử trên bồn cầu: Tìm hiểu về đối tượng kiểm thử.

Các đối tượng kiểm thử phổ biến nhất được dùng trong Android là Dữ liệu giảĐối tượng mô phỏng.

Trong nhiệm vụ này, bạn sẽ tạo một bản sao kiểm thử FakeDataSource để kiểm thử đơn vị DefaultTasksRepository tách biệt với các nguồn dữ liệu thực tế.

Bước 1: Tạo lớp FakeDataSource

Trong bước này, bạn sẽ tạo một lớp có tên là FakeDataSouce. Lớp này sẽ là một kiểm thử kép của LocalDataSourceRemoteDataSource.

  1. Trong nhóm tài nguyên test, hãy nhấp chuột phải rồi chọn New -> Package (Mới -> Gói).

  1. Tạo một gói data có một gói source bên trong.
  2. Tạo một lớp mới có tên là FakeDataSource trong gói data/source.

Bước 2: Triển khai giao diện TasksDataSource

Để có thể sử dụng lớp FakeDataSource mới làm đối tượng kiểm thử thay thế, lớp này phải có khả năng thay thế các nguồn dữ liệu khác. Các nguồn dữ liệu đó là TasksLocalDataSourceTasksRemoteDataSource.

  1. Lưu ý cách cả hai đều triển khai giao diện TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Yêu cầu FakeDataSource triển khai TasksDataSource:
class FakeDataSource : TasksDataSource {

}

Android Studio sẽ báo lỗi rằng bạn chưa triển khai các phương thức bắt buộc cho TasksDataSource.

  1. Sử dụng trình đơn sửa lỗi nhanh rồi chọn Implement members (Triển khai thành viên).


  1. Chọn tất cả các phương thức rồi nhấn OK.

Bước 3: Triển khai phương thức getTasks trong FakeDataSource

FakeDataSource là một loại đối tượng kiểm thử cụ thể được gọi là đối tượng giả. Đối tượng giả là một đối tượng kiểm thử có cách triển khai "hoạt động" của lớp, nhưng được triển khai theo cách phù hợp với các quy trình kiểm thử nhưng không phù hợp với phiên bản phát hành chính thức. Việc triển khai "đang hoạt động" có nghĩa là lớp sẽ tạo ra kết quả thực tế dựa trên dữ liệu đầu vào.

Ví dụ: nguồn dữ liệu giả của bạn sẽ không kết nối với mạng hoặc lưu bất cứ thứ gì vào cơ sở dữ liệu mà chỉ sử dụng một danh sách trong bộ nhớ. Điều này sẽ "hoạt động như bạn mong đợi" theo cách mà các phương thức để nhận hoặc lưu công việc sẽ trả về kết quả như mong đợi, nhưng bạn không bao giờ có thể sử dụng phương thức triển khai này trong quá trình sản xuất vì phương thức này không được lưu vào máy chủ hoặc cơ sở dữ liệu.

FakeDataSource

  • cho phép bạn kiểm thử mã trong DefaultTasksRepository mà không cần dựa vào cơ sở dữ liệu hoặc mạng thực.
  • cung cấp một phương thức triển khai "đủ thực tế" cho các kiểm thử.
  1. Thay đổi hàm khởi tạo FakeDataSource để tạo một var có tên là tasks. Đây là một MutableList<Task>? có giá trị mặc định là một danh sách trống có thể thay đổi.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Đây là danh sách các tác vụ "giả mạo" là phản hồi của cơ sở dữ liệu hoặc máy chủ. Hiện tại, mục tiêu là kiểm thử phương thức getTasks của kho lưu trữ . Thao tác này sẽ gọi các phương thức getTasks, deleteAllTaskssaveTask của nguồn dữ liệu .

Viết một phiên bản giả của các phương thức này:

  1. Write getTasks: Nếu tasks không phải là null, hãy trả về kết quả Success. Nếu tasksnull, hãy trả về kết quả Error.
  2. Viết deleteAllTasks: xoá danh sách việc cần làm có thể thay đổi.
  3. Viết saveTask: thêm việc cần làm vào danh sách.

Những phương thức được triển khai cho FakeDataSource sẽ có dạng như mã bên dưới.

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

Sau đây là các câu lệnh nhập nếu cần:

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

Điều này tương tự như cách hoạt động của nguồn dữ liệu cục bộ và từ xa thực tế.

Trong bước này, bạn sẽ sử dụng một kỹ thuật gọi là chèn phần phụ thuộc theo cách thủ công để có thể sử dụng đối tượng kiểm thử giả mà bạn vừa tạo.

Vấn đề chính là bạn có một FakeDataSource, nhưng không rõ cách bạn sử dụng nó trong các kiểm thử. Bạn cần thay thế TasksRemoteDataSourceTasksLocalDataSource, nhưng chỉ trong các kiểm thử. Cả TasksRemoteDataSourceTasksLocalDataSource đều là phần phụ thuộc của DefaultTasksRepository, tức là DefaultTasksRepositories yêu cầu hoặc "phụ thuộc" vào các lớp này để chạy.

Hiện tại, các phần phụ thuộc được tạo trong phương thức init của 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
}

Vì bạn đang tạo và chỉ định taskLocalDataSourcetasksRemoteDataSource bên trong DefaultTasksRepository, nên về cơ bản, chúng được mã hoá cứng. Bạn không thể thay thế bằng kiểm thử kép.

Thay vào đó, bạn nên cung cấp các nguồn dữ liệu này cho lớp thay vì mã hoá cứng. Việc cung cấp các phần phụ thuộc được gọi là chèn phần phụ thuộc. Có nhiều cách để cung cấp các phần phụ thuộc, do đó có nhiều loại phương thức chèn phần phụ thuộc.

Constructor Dependency Injection (Chèn phần phụ thuộc của hàm khởi tạo) cho phép bạn hoán đổi trong đối tượng kiểm thử giả bằng cách truyền đối tượng đó vào hàm khởi tạo.

Không chèn

Tiêm

Bước 1: Sử dụng tính năng Chèn phần phụ thuộc của hàm khởi tạo trong DefaultTasksRepository

  1. Thay đổi hàm dựng DefaultTaskRepository từ việc nhận Application thành việc nhận cả nguồn dữ liệu và trình điều phối coroutine (bạn cũng cần hoán đổi cho các kiểm thử của mình – điều này được mô tả chi tiết hơn trong phần thứ ba của bài học về 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. Vì bạn đã truyền các phần phụ thuộc vào nên hãy xoá phương thức init. Bạn không cần tạo các phần phụ thuộc nữa.
  2. Đồng thời xoá các biến phiên bản cũ. Bạn đang xác định chúng trong hàm khởi tạo:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Cuối cùng, hãy cập nhật phương thức getRepository để sử dụng hàm khởi tạo mới:

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

Giờ đây, bạn đang sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo!

Bước 2: Sử dụng FakeDataSource trong các kiểm thử

Giờ đây, mã của bạn đang sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo, bạn có thể sử dụng nguồn dữ liệu giả để kiểm thử DefaultTasksRepository.

  1. Nhấp chuột phải vào tên lớp DefaultTasksRepository rồi chọn Generate (Tạo), sau đó chọn Test (Kiểm thử).
  2. Làm theo lời nhắc để tạo DefaultTasksRepositoryTest trong nhóm tài nguyên kiểm thử.
  3. Ở đầu lớp DefaultTasksRepositoryTest mới, hãy thêm các biến thành viên bên dưới để biểu thị dữ liệu trong các nguồn dữ liệu giả.

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. Tạo 3 biến, 2 biến thành phần FakeDataSource (mỗi biến cho một nguồn dữ liệu cho kho lưu trữ của bạn) và một biến cho DefaultTasksRepository mà bạn sẽ kiểm thử.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Tạo một phương thức để thiết lập và khởi chạy DefaultTasksRepository có thể kiểm thử. DefaultTasksRepository này sẽ sử dụng đối tượng kiểm thử thay thế FakeDataSource.

  1. Tạo một hàm có tên là createRepository rồi chú thích hàm đó bằng @Before.
  2. Khởi tạo nguồn dữ liệu giả bằng cách sử dụng danh sách remoteTaskslocalTasks.
  3. Khởi tạo tasksRepository bằng cách sử dụng 2 nguồn dữ liệu giả mà bạn vừa tạo và Dispatchers.Unconfined.

Phương thức cuối cùng sẽ có dạng như mã bên dưới.

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

Bước 3: Viết kiểm thử DefaultTasksRepository getTasks()

Đã đến lúc viết một kiểm thử DefaultTasksRepository!

  1. Viết một kiểm thử cho phương thức getTasks của kho lưu trữ. Kiểm tra để đảm bảo rằng khi bạn gọi getTasks bằng true (nghĩa là bạn nên tải lại từ nguồn dữ liệu từ xa), thì phương thức này sẽ trả về dữ liệu từ nguồn dữ liệu từ xa (thay vì nguồn dữ liệu cục bộ).

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

Bạn sẽ gặp lỗi khi gọi getTasks:

Bước 4: Thêm runBlockingTest

Lỗi coroutine dự kiến sẽ xảy ra vì getTasks là một hàm suspend và bạn cần khởi chạy một coroutine để gọi hàm này. Để làm việc đó, bạn cần có một phạm vi coroutine. Để giải quyết lỗi này, bạn sẽ cần thêm một số phần phụ thuộc gradle để xử lý việc khởi chạy các coroutine trong quy trình kiểm thử.

  1. Thêm các phần phụ thuộc cần thiết để kiểm thử coroutine vào nhóm nguồn kiểm thử bằng cách sử dụng testImplementation.

app/build.gradle

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

Đừng quên đồng bộ hoá!

kotlinx-coroutines-test là thư viện kiểm thử coroutine, dành riêng cho việc kiểm thử coroutine. Để chạy các kiểm thử, hãy dùng hàm runBlockingTest. Đây là một hàm do thư viện kiểm thử coroutine cung cấp. Hàm này nhận một khối mã rồi chạy khối mã này trong một ngữ cảnh coroutine đặc biệt, chạy đồng bộ và ngay lập tức, nghĩa là các thao tác sẽ diễn ra theo một thứ tự xác định. Về cơ bản, điều này khiến các coroutine của bạn chạy như các coroutine không phải là coroutine, vì vậy, nó được dùng để kiểm thử mã.

Sử dụng runBlockingTest trong các lớp kiểm thử khi bạn đang gọi một hàm suspend. Bạn sẽ tìm hiểu thêm về cách hoạt động của runBlockingTest và cách kiểm thử coroutine trong lớp học lập trình tiếp theo của loạt lớp học lập trình này.

  1. Thêm @ExperimentalCoroutinesApi phía trên lớp. Điều này cho thấy bạn biết mình đang sử dụng một API coroutine thử nghiệm (runBlockingTest) trong lớp. Nếu không có thông tin này, bạn sẽ nhận được một cảnh báo.
  2. Quay lại DefaultTasksRepositoryTest, hãy thêm runBlockingTest để lấy toàn bộ bài kiểm thử của bạn làm "khối" mã

Bài kiểm thử cuối cùng này có dạng như mã bên dưới.

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. Chạy kiểm thử getTasks_requestsAllTasksFromRemoteDataSource mới và xác nhận rằng kiểm thử hoạt động và lỗi đã biến mất!

Bạn vừa xem cách kiểm thử đơn vị một kho lưu trữ. Trong các bước tiếp theo, bạn sẽ sử dụng lại tính năng chèn phần phụ thuộc và tạo một đối tượng kiểm thử giả khác. Lần này, bạn sẽ cho thấy cách viết các bài kiểm thử đơn vị và kiểm thử tích hợp cho các mô hình hiển thị.

Các bài kiểm thử đơn vị chỉ kiểm thử lớp hoặc phương thức mà bạn quan tâm. Đây được gọi là kiểm thử độc lập, trong đó bạn tách biệt rõ ràng "đơn vị" và chỉ kiểm thử mã thuộc đơn vị đó.

Vì vậy, TasksViewModelTest chỉ nên kiểm thử mã TasksViewModel, chứ không nên kiểm thử trong cơ sở dữ liệu, mạng hoặc các lớp kho lưu trữ. Do đó, đối với các mô hình hiển thị, giống như bạn vừa làm cho kho lưu trữ, bạn sẽ tạo một kho lưu trữ giả và áp dụng tính năng chèn phần phụ thuộc để sử dụng kho lưu trữ đó trong các quy trình kiểm thử.

Trong nhiệm vụ này, bạn sẽ áp dụng tính năng chèn phần phụ thuộc vào các mô hình khung hiển thị.

Bước 1. Tạo một giao diện TasksRepository

Bước đầu tiên để sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo là tạo một giao diện chung được chia sẻ giữa lớp giả và lớp thực.

Trong thực tế, tính năng này trông như thế nào? Hãy xem TasksRemoteDataSource, TasksLocalDataSourceFakeDataSource, bạn sẽ thấy rằng tất cả đều dùng chung một giao diện: TasksDataSource. Điều này cho phép bạn nói trong hàm khởi tạo của DefaultTasksRepository rằng bạn sẽ nhận một TasksDataSource.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

Đây là những gì cho phép chúng ta hoán đổi trong FakeDataSource của bạn!

Tiếp theo, hãy tạo một giao diện cho DefaultTasksRepository, như bạn đã làm cho các nguồn dữ liệu. Bạn cần thêm tất cả các phương thức công khai (giao diện API công khai) của DefaultTasksRepository.

  1. Mở DefaultTasksRepository rồi nhấp chuột phải vào tên lớp. Sau đó, chọn Refactor -> Extract -> Interface (Tái cấu trúc -> Trích xuất -> Giao diện).

  1. Chọn Trích xuất vào tệp riêng biệt.

  1. Trong cửa sổ Extract Interface (Trích xuất giao diện), hãy thay đổi tên giao diện thành TasksRepository.
  2. Trong phần Members to form interface (Thành viên tạo giao diện), hãy đánh dấu vào tất cả thành viên ngoại trừ 2 thành viên đồng hành và các phương thức riêng tư.


  1. Nhấp vào Refactor (Tái cấu trúc). Giao diện TasksRepository mới sẽ xuất hiện trong gói data/source .

DefaultTasksRepository hiện triển khai TasksRepository.

  1. Chạy ứng dụng (không phải các kiểm thử) để đảm bảo mọi thứ vẫn hoạt động bình thường.

Bước 2. Tạo FakeTestRepository

Giờ đây, bạn đã có giao diện, bạn có thể tạo kiểm thử kép DefaultTaskRepository.

  1. Trong tập hợp nguồn test, trong data/source, hãy tạo tệp và lớp Kotlin FakeTestRepository.kt rồi mở rộng từ giao diện TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Bạn sẽ được thông báo rằng bạn cần triển khai các phương thức giao diện.

  1. Di chuột lên lỗi cho đến khi bạn thấy trình đơn đề xuất, sau đó nhấp và chọn Implement members (Triển khai thành phần).
  1. Chọn tất cả các phương thức rồi nhấn OK.

Bước 3. Triển khai các phương thức FakeTestRepository

Giờ đây, bạn có một lớp FakeTestRepository với các phương thức "chưa triển khai". Tương tự như cách bạn triển khai FakeDataSource, FakeTestRepository sẽ được hỗ trợ bởi một cấu trúc dữ liệu, thay vì phải xử lý một hoạt động hoà giải phức tạp giữa các nguồn dữ liệu cục bộ và từ xa.

Xin lưu ý rằng FakeTestRepository không cần sử dụng FakeDataSource hoặc bất kỳ thứ gì tương tự; nó chỉ cần trả về các đầu ra giả thực tế cho các đầu vào. Bạn sẽ dùng LinkedHashMap để lưu trữ danh sách việc cần làm và MutableLiveData cho các việc cần làm có thể quan sát.

  1. Trong FakeTestRepository, hãy thêm cả biến LinkedHashMap đại diện cho danh sách việc cần làm hiện tại và MutableLiveData cho các việc cần làm có thể quan sát được.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

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

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


    // Rest of class
}

Triển khai các phương thức sau:

  1. getTasks – Phương thức này sẽ lấy tasksServiceData và chuyển thành một danh sách bằng cách sử dụng tasksServiceData.values.toList(), sau đó trả về danh sách đó dưới dạng kết quả Success.
  2. refreshTasks – Cập nhật giá trị của observableTasks thành giá trị do getTasks() trả về.
  3. observeTasks – Tạo một coroutine bằng runBlocking và chạy refreshTasks, sau đó trả về observableTasks.

Dưới đây là mã cho các phương thức đó.

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

}

Bước 4. Thêm một phương thức để kiểm thử addTasks

Khi kiểm thử, bạn nên có sẵn một số Tasks trong kho lưu trữ. Bạn có thể gọi saveTask nhiều lần, nhưng để đơn giản hoá việc này, hãy thêm một phương thức hỗ trợ dành riêng cho các chương trình kiểm thử cho phép bạn thêm các tác vụ.

  1. Thêm phương thức addTasks. Phương thức này sẽ nhận một vararg gồm các việc cần làm, thêm từng việc vào HashMap, rồi làm mới các việc cần làm.

FakeTestRepository.kt

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

Tại thời điểm này, bạn có một kho lưu trữ giả để kiểm thử với một số phương thức chính đã triển khai. Tiếp theo, hãy sử dụng hàm này trong các kiểm thử của bạn!

Trong nhiệm vụ này, bạn sẽ sử dụng một lớp giả bên trong ViewModel. Sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo để lấy 2 nguồn dữ liệu thông qua tính năng chèn phần phụ thuộc của hàm khởi tạo bằng cách thêm một biến TasksRepository vào hàm khởi tạo của TasksViewModel.

Quá trình này có chút khác biệt với các mô hình hiển thị vì bạn không tạo trực tiếp các mô hình đó. Ví dụ:

class TasksFragment : Fragment() {

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

}


Như trong mã ở trên, bạn đang sử dụng phần uỷ quyền thuộc tính viewModel's để tạo mô hình hiển thị. Để thay đổi cách tạo mô hình hiển thị, bạn cần thêm và sử dụng một ViewModelProvider.Factory. Nếu chưa quen thuộc với ViewModelProvider.Factory, bạn có thể tìm hiểu thêm về công cụ này tại đây.

Bước 1. Tạo và sử dụng ViewModelFactory trong TasksViewModel

Bạn bắt đầu bằng cách cập nhật các lớp và kiểm thử liên quan đến màn hình Tasks.

  1. Mở TasksViewModel.
  2. Thay đổi hàm khởi tạo của TasksViewModel để nhận TasksRepository thay vì tạo hàm này bên trong lớp.

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 
}

Vì đã thay đổi hàm khởi tạo, nên giờ đây, bạn cần sử dụng một phương thức khởi tạo để tạo TasksViewModel. Đặt lớp tạo trong cùng tệp với TasksViewModel, nhưng bạn cũng có thể đặt lớp này trong tệp riêng.

  1. Ở cuối tệp TasksViewModel, bên ngoài lớp, hãy thêm một TasksViewModelFactory nhận TasksRepository thuần tuý.

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


Đây là cách tiêu chuẩn để bạn thay đổi cách tạo ViewModel. Giờ đây, bạn có thể sử dụng đối tượng này ở bất cứ nơi nào bạn tạo mô hình hiển thị.

  1. Cập nhật TasksFragment để sử dụng nhà máy.

TasksFragment.kt

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

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Chạy mã ứng dụng và đảm bảo mọi thứ vẫn hoạt động!

Bước 2. Sử dụng FakeTestRepository trong TasksViewModelTest

Giờ đây, thay vì sử dụng kho lưu trữ thực trong các kiểm thử mô hình hiển thị, bạn có thể sử dụng kho lưu trữ giả.

  1. Mở TasksViewModelTest.
  2. Thêm thuộc tính FakeTestRepository vào 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. Cập nhật phương thức setupViewModel để tạo một FakeTestRepository có 3 tác vụ, sau đó tạo tasksViewModel bằng kho lưu trữ này.

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. Vì không còn dùng mã AndroidX Test ApplicationProvider.getApplicationContext nữa, bạn cũng có thể xoá chú thích @RunWith(AndroidJUnit4::class).
  2. Chạy các kiểm thử để đảm bảo tất cả vẫn hoạt động!

Bằng cách sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo, giờ đây, bạn đã xoá DefaultTasksRepository dưới dạng một phần phụ thuộc và thay thế bằng FakeTestRepository trong các kiểm thử.

Bước 3. Cũng cập nhật mảnh TaskDetail và ViewModel

Thực hiện chính xác các thay đổi tương tự cho TaskDetailFragmentTaskDetailViewModel. Thao tác này sẽ chuẩn bị mã cho khi bạn viết các bài kiểm thử TaskDetail tiếp theo.

  1. Mở TaskDetailViewModel.
  2. Cập nhật hàm khởi tạo:

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. Ở cuối tệp TaskDetailViewModel bên ngoài lớp, hãy thêm một 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. Cập nhật TasksFragment để sử dụng nhà máy.

TasksFragment.kt

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

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Chạy mã của bạn và đảm bảo mọi thứ đều hoạt động.

Giờ đây, bạn có thể sử dụng FakeTestRepository thay vì kho lưu trữ thực trong TasksFragmentTasksDetailFragment.

Tiếp theo, bạn sẽ viết các bài kiểm thử tích hợp để kiểm thử các hoạt động tương tác giữa mảnh và mô hình hiển thị. Bạn sẽ biết liệu mã mô hình hiển thị có cập nhật giao diện người dùng một cách thích hợp hay không. Để làm việc này, bạn có thể sử dụng

  • mẫu ServiceLocator
  • thư viện Espresso và Mockito

Kiểm thử tích hợp kiểm thử hoạt động tương tác của một số lớp để đảm bảo các lớp này hoạt động như dự kiến khi được dùng cùng nhau. Bạn có thể chạy các kiểm thử này trên máy cục bộ (nhóm tài nguyên test) hoặc dưới dạng kiểm thử đo lường (nhóm tài nguyên androidTest).

Trong trường hợp của bạn, bạn sẽ lấy từng mảnh và viết các kiểm thử tích hợp cho mảnh và mô hình hiển thị để kiểm thử các tính năng chính của mảnh.

Bước 1. Thêm phần phụ thuộc vào Gradle

  1. Thêm các phần phụ thuộc gradle sau đây.

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"

Các phần phụ thuộc này bao gồm:

  • junit:junit – JUnit, cần thiết để viết các câu lệnh kiểm thử cơ bản.
  • androidx.test:core – Thư viện kiểm thử AndroidX Core
  • kotlinx-coroutines-test – Thư viện kiểm thử coroutine
  • androidx.fragment:fragment-testing – Thư viện kiểm thử AndroidX để tạo các mảnh trong quy trình kiểm thử và thay đổi trạng thái của các mảnh đó.

Vì bạn sẽ sử dụng các thư viện này trong nhóm tài nguyên androidTest, hãy dùng androidTestImplementation để thêm các thư viện này làm phần phụ thuộc.

Bước 2. Tạo lớp TaskDetailFragmentTest

TaskDetailFragment cho biết thông tin về một tác vụ.

Bạn sẽ bắt đầu bằng cách viết một kiểm thử phân mảnh cho TaskDetailFragment vì phân mảnh này có chức năng khá cơ bản so với các phân mảnh khác.

  1. Mở taskdetail.TaskDetailFragment.
  2. Tạo một quy trình kiểm thử cho TaskDetailFragment, như bạn đã làm trước đây. Chấp nhận các lựa chọn mặc định và đặt lựa chọn đó vào nhóm tài nguyên androidTest (KHÔNG phải nhóm tài nguyên test).

  1. Thêm các chú giải sau vào lớp TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

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

}

Mục đích của những chú thích này là:

  • @MediumTest – Đánh dấu bài kiểm thử là bài kiểm thử tích hợp "thời gian chạy trung bình" (so với bài kiểm thử đơn vị @SmallTest và bài kiểm thử toàn diện @LargeTest). Điều này giúp bạn nhóm và chọn kích thước của thử nghiệm cần chạy.
  • @RunWith(AndroidJUnit4::class) – Được dùng trong mọi lớp sử dụng AndroidX Test.

Bước 3. Khởi chạy một mảnh từ một hoạt động kiểm thử

Trong nhiệm vụ này, bạn sẽ khởi chạy TaskDetailFragment bằng Thư viện kiểm thử AndroidX. FragmentScenario là một lớp trong Kiểm thử AndroidX, bao bọc một mảnh và cho phép bạn kiểm soát trực tiếp vòng đời của mảnh để kiểm thử. Để viết các kiểm thử cho mảnh, bạn sẽ tạo một FragmentScenario cho mảnh mà bạn đang kiểm thử (TaskDetailFragment).

  1. Sao chép kiểm thử này vào 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)

    }

Mã này ở trên:

  • Tạo một việc cần làm.
  • Tạo một Bundle, đại diện cho các đối số của mảnh cho tác vụ được truyền vào mảnh).
  • Hàm launchFragmentInContainer tạo một FragmentScenario, cùng với gói và giao diện này.

Đây chưa phải là một kiểm thử hoàn chỉnh vì chưa xác nhận điều gì. Hiện tại, hãy chạy kiểm thử và quan sát những gì xảy ra.

  1. Đây là một kiểm thử đo lường, vì vậy, hãy đảm bảo trình mô phỏng hoặc thiết bị của bạn hiển thị được.
  2. Chạy kiểm thử.

Một số điều sẽ xảy ra.

  • Trước tiên, vì đây là một kiểm thử đo lường, nên kiểm thử sẽ chạy trên thiết bị thực (nếu được kết nối) hoặc trình mô phỏng.
  • Thao tác này sẽ khởi chạy mảnh.
  • Lưu ý cách mà mảnh này không điều hướng qua bất kỳ mảnh nào khác hoặc không có bất kỳ trình đơn nào liên kết với hoạt động – mảnh này chỉ là mảnh.

Cuối cùng, hãy quan sát kỹ và nhận thấy rằng đoạn mã này có nội dung "Không có dữ liệu" vì không tải thành công dữ liệu nhiệm vụ.

Cả hai kiểm thử của bạn đều cần tải TaskDetailFragment (bạn đã thực hiện) và xác nhận rằng dữ liệu đã được tải chính xác. Tại sao không có dữ liệu? Nguyên nhân là do bạn đã tạo một việc cần làm nhưng chưa lưu vào kho lưu trữ.

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

    }

Bạn có FakeTestRepository này, nhưng bạn cần một cách nào đó để thay thế kho lưu trữ thực bằng kho lưu trữ giả cho phân đoạn của mình. Bạn sẽ thực hiện việc này ở bước tiếp theo!

Trong nhiệm vụ này, bạn sẽ cung cấp kho lưu trữ giả cho mảnh bằng cách sử dụng ServiceLocator. Điều này sẽ cho phép bạn viết các bài kiểm thử tích hợp mảnh và mô hình hiển thị.

Bạn không thể sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo ở đây, như trước đây, khi cần cung cấp một phần phụ thuộc cho mô hình hiển thị hoặc kho lưu trữ. Tính năng chèn phần phụ thuộc bằng hàm khởi tạo yêu cầu bạn tạo lớp. Các mảnh và hoạt động là ví dụ về những lớp mà bạn không tạo và thường không có quyền truy cập vào hàm khởi tạo.

Vì không tạo mảnh, nên bạn không thể sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo để hoán đổi đối tượng kiểm thử thay thế kho lưu trữ (FakeTestRepository) cho mảnh. Thay vào đó, hãy sử dụng mẫu Service Locator. Mẫu Công cụ định vị dịch vụ là một giải pháp thay thế cho tính năng Chèn phần phụ thuộc. Việc này liên quan đến việc tạo một lớp singleton có tên là "Service Locator" (Công cụ định vị dịch vụ), có mục đích là cung cấp các phần phụ thuộc cho cả mã thông thường và mã kiểm thử. Trong mã ứng dụng thông thường (nhóm tài nguyên main), tất cả các phần phụ thuộc này đều là phần phụ thuộc của ứng dụng thông thường. Đối với các kiểm thử, bạn sửa đổi Công cụ định vị dịch vụ để cung cấp các phiên bản kiểm thử kép của các phần phụ thuộc.

Không sử dụng Trình định vị dịch vụ


Sử dụng Service Locator

Đối với ứng dụng trong lớp học lập trình này, hãy làm như sau:

  1. Tạo một lớp Service Locator có thể tạo và lưu trữ một kho lưu trữ. Theo mặc định, nó sẽ tạo một kho lưu trữ "bình thường".
  2. Tái cấu trúc mã của bạn để khi bạn cần một kho lưu trữ, hãy sử dụng Service Locator.
  3. Trong lớp kiểm thử, hãy gọi một phương thức trên Service Locator (Bộ định vị dịch vụ) để hoán đổi kho lưu trữ "bình thường" bằng bản sao kiểm thử.

Bước 1. Tạo ServiceLocator

Hãy tạo một lớp ServiceLocator. Thành phần này sẽ nằm trong bộ nguồn chính cùng với phần còn lại của mã ứng dụng vì được mã ứng dụng chính sử dụng.

Lưu ý: ServiceLocator là một singleton, vì vậy, hãy dùng từ khoá object Kotlin cho lớp.

  1. Tạo tệp ServiceLocator.kt ở cấp cao nhất của nhóm tài nguyên chính.
  2. Xác định một object có tên là ServiceLocator.
  3. Tạo các biến thực thể databaserepository, đồng thời đặt cả hai thành null.
  4. Chú thích kho lưu trữ bằng @Volatile vì kho lưu trữ có thể được dùng bởi nhiều luồng (@Volatile được giải thích chi tiết tại đây).

Mã của bạn sẽ có dạng như dưới đây.

object ServiceLocator {

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

}

Hiện tại, điều duy nhất mà ServiceLocator của bạn cần làm là biết cách trả về một TasksRepository. Phương thức này sẽ trả về một DefaultTasksRepository đã tồn tại trước đó hoặc tạo và trả về một DefaultTasksRepository mới (nếu cần).

Xác định các hàm sau:

  1. provideTasksRepository – Cung cấp một kho lưu trữ hiện có hoặc tạo một kho lưu trữ mới. Phương thức này phải là synchronized trên this để tránh vô tình tạo ra hai thực thể kho lưu trữ trong các trường hợp có nhiều luồng đang chạy.
  2. createTasksRepository – Mã để tạo một kho lưu trữ mới. Sẽ gọi createTaskLocalDataSource và tạo một TasksRemoteDataSource mới.
  3. createTaskLocalDataSource – Mã để tạo một nguồn dữ liệu cục bộ mới. Sẽ gọi số createDataBase.
  4. createDataBase – Mã để tạo cơ sở dữ liệu mới.

Sau đây là mã hoàn chỉnh.

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

Bước 2. Sử dụng ServiceLocator trong Ứng dụng

Bạn sẽ thực hiện thay đổi đối với mã ứng dụng chính (không phải các kiểm thử) để tạo kho lưu trữ ở một nơi, đó là ServiceLocator.

Điều quan trọng là bạn chỉ tạo một thực thể của lớp kho lưu trữ. Để đảm bảo điều này, bạn sẽ sử dụng Service locator (Trình định vị dịch vụ) trong lớp Application.

  1. Ở cấp cao nhất của hệ phân cấp gói, hãy mở TodoApplication và tạo một val cho kho lưu trữ của bạn, đồng thời chỉ định cho kho lưu trữ đó một kho lưu trữ được lấy bằng 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())
    }
}

Giờ đây, khi đã tạo một kho lưu trữ trong ứng dụng, bạn có thể xoá phương thức getRepository cũ trong DefaultTasksRepository.

  1. Mở DefaultTasksRepository rồi xoá đối tượng đồng hành.

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

Giờ đây, ở mọi nơi bạn đang sử dụng getRepository, hãy sử dụng taskRepository của ứng dụng. Điều này đảm bảo rằng thay vì tạo trực tiếp kho lưu trữ, bạn sẽ nhận được bất kỳ kho lưu trữ nào mà ServiceLocator cung cấp.

  1. Mở TaskDetailFragement và tìm lệnh gọi đến getRepository ở đầu lớp.
  2. Thay thế lệnh gọi này bằng một lệnh gọi lấy kho lưu trữ từ 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. Làm tương tự cho 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. Đối với StatisticsViewModelAddEditTaskViewModel, hãy cập nhật mã thu thập kho lưu trữ để sử dụng kho lưu trữ từ TodoApplication.

TasksFragment.kt

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



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Chạy ứng dụng của bạn (không phải kiểm thử)!

Vì bạn chỉ tái cấu trúc nên ứng dụng sẽ chạy bình thường mà không gặp vấn đề gì.

Bước 3. Tạo FakeAndroidTestRepository

Bạn đã có một FakeTestRepository trong bộ nguồn kiểm thử. Theo mặc định, bạn không thể chia sẻ các lớp kiểm thử giữa các nhóm tài nguyên testandroidTest. Vì vậy, bạn cần tạo một lớp FakeTestRepository trùng lặp trong tập hợp nguồn androidTest và gọi lớp đó là FakeAndroidTestRepository.

  1. Nhấp chuột phải vào tập hợp nguồn androidTest rồi tạo một gói dữ liệu. Nhấp chuột phải một lần nữa và tạo một gói nguồn .
  2. Tạo một lớp mới trong gói nguồn này có tên là FakeAndroidTestRepository.kt.
  3. Sao chép đoạn mã sau vào lớp đó.

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

Bước 4. Chuẩn bị ServiceLocator cho các bài kiểm thử

Được rồi, đã đến lúc sử dụng ServiceLocator để hoán đổi trong các đối tượng kiểm thử khi kiểm thử. Để làm việc đó, bạn cần thêm một số mã vào mã ServiceLocator.

  1. Mở ServiceLocator.kt.
  2. Đánh dấu phương thức thiết lập cho tasksRepository@VisibleForTesting. Chú thích này là một cách để thể hiện rằng lý do phương thức thiết lập là công khai là do kiểm thử.

ServiceLocator.kt

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

Cho dù bạn chạy kiểm thử một mình hay trong một nhóm kiểm thử, các kiểm thử của bạn đều phải chạy giống hệt nhau. Điều này có nghĩa là các kiểm thử của bạn không được có hành vi phụ thuộc lẫn nhau (tức là tránh chia sẻ các đối tượng giữa các kiểm thử).

ServiceLocator là một singleton, nên có khả năng bị vô tình chia sẻ giữa các kiểm thử. Để tránh trường hợp này, hãy tạo một phương thức đặt lại trạng thái ServiceLocator đúng cách giữa các bài kiểm thử.

  1. Thêm một biến thực thể có tên là lock với giá trị Any.

ServiceLocator.kt

private val lock = Any()
  1. Thêm một phương thức dành riêng cho việc kiểm thử có tên là resetRepository. Phương thức này sẽ xoá cơ sở dữ liệu và đặt cả kho lưu trữ cũng như cơ sở dữ liệu thành giá trị rỗng.

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

Bước 5. Sử dụng ServiceLocator

Trong bước này, bạn sẽ dùng ServiceLocator.

  1. Mở TaskDetailFragmentTest.
  2. Khai báo một biến lateinit TasksRepository.
  3. Thêm một phương thức thiết lập và một phương thức huỷ để thiết lập FakeAndroidTestRepository trước mỗi lần kiểm thử và dọn dẹp sau mỗi lần kiểm thử.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

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

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Gói phần nội dung hàm của activeTaskDetails_DisplayedInUi() trong runBlockingTest.
  2. Lưu activeTask trong kho lưu trữ trước khi chạy mảnh.
repository.saveTask(activeTask)

Bài kiểm thử cuối cùng sẽ có dạng như mã bên dưới.

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. Chú giải toàn bộ lớp bằng @ExperimentalCoroutinesApi.

Khi hoàn tất, mã sẽ có dạng như sau.

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. Chạy kiểm thử activeTaskDetails_DisplayedInUi().

Giống như trước đây, bạn sẽ thấy mảnh này, ngoại trừ lần này, vì bạn đã thiết lập kho lưu trữ đúng cách nên giờ đây, mảnh này sẽ hiển thị thông tin về tác vụ.


Trong bước này, bạn sẽ sử dụng thư viện kiểm thử giao diện người dùng Espresso để hoàn tất quy trình kiểm thử tích hợp đầu tiên. Bạn đã cấu trúc mã để có thể thêm các bài kiểm thử có câu lệnh xác nhận cho giao diện người dùng. Để làm việc đó, bạn sẽ dùng thư viện kiểm thử Espresso.

Espresso giúp bạn:

  • Tương tác với các khung hiển thị, chẳng hạn như nhấp vào nút, trượt thanh hoặc di chuyển xuống trên màn hình.
  • Xác nhận rằng một số thành phần hiển thị đang ở trên màn hình hoặc ở một trạng thái nhất định (chẳng hạn như chứa văn bản cụ thể hoặc hộp đánh dấu đã được đánh dấu, v.v.).

Bước 1. Lưu ý về phần phụ thuộc Gradle

Bạn sẽ có phần phụ thuộc Espresso chính vì phần này được đưa vào các dự án Android theo mặc định.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core – Phần phụ thuộc Espresso cốt lõi này được thêm theo mặc định khi bạn tạo một dự án Android mới. Tệp này chứa mã kiểm thử cơ bản cho hầu hết các khung hiển thị và thao tác trên các khung hiển thị đó.

Bước 2. Tắt ảnh động

Các kiểm thử Espresso chạy trên một thiết bị thực và do đó, về bản chất là các kiểm thử đo lường. Một vấn đề phát sinh là ảnh động: Nếu ảnh động bị trễ và bạn cố gắng kiểm thử xem một khung hiển thị có trên màn hình hay không, nhưng khung hiển thị đó vẫn đang tạo ảnh động, thì Espresso có thể vô tình làm cho một quy trình kiểm thử thất bại. Điều này có thể khiến các kiểm thử Espresso không ổn định.

Đối với kiểm thử giao diện người dùng Espresso, phương pháp hay nhất là tắt ảnh động (thử nghiệm của bạn cũng sẽ chạy nhanh hơn!):

  1. Trên thiết bị kiểm thử, hãy chuyển đến phần Cài đặt > Tuỳ chọn cho nhà phát triển.
  2. Tắt 3 chế độ cài đặt sau: Tỷ lệ hình động của cửa sổ, Tỷ lệ hình động chuyển tiếpTỷ lệ thời lượng của trình tạo hình động.

Bước 3. Xem quy trình kiểm thử Espresso

Trước khi bạn viết một kiểm thử Espresso, hãy xem một số mã Espresso.

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

Câu lệnh này sẽ tìm thành phần hiển thị hộp đánh dấu có mã nhận dạng task_detail_complete_checkbox, nhấp vào thành phần đó, sau đó xác nhận rằng thành phần đó đã được đánh dấu.

Hầu hết các câu lệnh Espresso đều có 4 phần:

1. Phương thức Espresso tĩnh

onView

onView là một ví dụ về phương thức Espresso tĩnh giúp bắt đầu một câu lệnh Espresso. onView là một trong những lựa chọn phổ biến nhất, nhưng có những lựa chọn khác, chẳng hạn như onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId là một ví dụ về ViewMatcher. Lớp này nhận một khung hiển thị theo mã nhận dạng của khung hiển thị đó. Bạn có thể tìm các đối tượng so khớp khung hiển thị khác trong tài liệu.

3. ViewAction

perform(click())

Phương thức perform lấy một ViewAction. ViewAction là một thao tác có thể thực hiện trên khung hiển thị, ví dụ: nhấp vào khung hiển thị.

4. ViewAssertion

check(matches(isChecked()))

check nhận một ViewAssertion. ViewAssertion kiểm tra hoặc khẳng định điều gì đó về khung hiển thị. ViewAssertion phổ biến nhất mà bạn sẽ sử dụng là câu khẳng định matches. Để hoàn tất câu khẳng định, hãy dùng một ViewMatcher khác, trong trường hợp này là isChecked.

Xin lưu ý rằng bạn không phải lúc nào cũng gọi cả performcheck trong một câu lệnh Espresso. Bạn có thể có các câu lệnh chỉ đưa ra một khẳng định bằng cách sử dụng check hoặc chỉ thực hiện một ViewAction bằng cách sử dụng perform.

  1. Mở TaskDetailFragmentTest.kt.
  2. Cập nhật kiểm thử 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())))
    }

Sau đây là các câu lệnh nhập, nếu cần:

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. Mọi nội dung sau chú thích // THEN đều sử dụng Espresso. Kiểm tra cấu trúc kiểm thử và việc sử dụng withId, đồng thời kiểm tra để đưa ra các khẳng định về giao diện của trang chi tiết.
  2. Chạy kiểm thử và xác nhận là bài kiểm thử đạt.

Bước 4. Không bắt buộc, Viết bài kiểm thử Espresso của riêng bạn

Bây giờ, hãy tự viết một bài kiểm thử.

  1. Tạo một bài kiểm thử mới có tên là completedTaskDetails_DisplayedInUi rồi sao chép mã khung này.

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. Nhìn vào bài kiểm tra trước, hãy hoàn thành bài kiểm tra này.
  2. Chạy và xác nhận bài kiểm thử đạt.

completedTaskDetails_DisplayedInUi hoàn chỉnh sẽ có dạng như mã này.

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

Trong bước cuối cùng này, bạn sẽ tìm hiểu cách kiểm thử Thành phần điều hướng bằng một loại đối tượng kiểm thử thay thế khác có tên là đối tượng mô phỏng và thư viện kiểm thử Mockito.

Trong lớp học lập trình này, bạn đã sử dụng một bản sao kiểm thử có tên là giả lập. Dữ liệu giả là một trong nhiều loại dữ liệu kiểm thử kép. Bạn nên sử dụng đối tượng kiểm thử nào để kiểm thử Thành phần điều hướng?

Hãy nghĩ về cách điều hướng. Hãy tưởng tượng bạn nhấn vào một trong các tác vụ trong TasksFragment để chuyển đến màn hình chi tiết của tác vụ.

Dưới đây là mã trong TasksFragment chuyển đến màn hình chi tiết của một việc cần làm khi người dùng nhấn vào.

TasksFragment.kt

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


Quá trình điều hướng xảy ra do lệnh gọi đến phương thức navigate. Nếu cần viết một câu lệnh xác nhận, thì không có cách nào đơn giản để kiểm thử xem bạn đã chuyển đến TaskDetailFragment hay chưa. Thao tác điều hướng là một thao tác phức tạp, không dẫn đến kết quả rõ ràng hoặc thay đổi trạng thái, ngoài việc khởi tạo TaskDetailFragment.

Bạn có thể khẳng định rằng phương thức navigate đã được gọi bằng tham số hành động chính xác. Đây chính xác là những gì mà một đối tượng kiểm thử mô phỏng thực hiện – đối tượng này kiểm tra xem các phương thức cụ thể có được gọi hay không.

Mockito là một khung để tạo các kiểm thử kép. Mặc dù từ mô phỏng được dùng trong API và tên, nhưng không chỉ để tạo các mô phỏng. Nó cũng có thể tạo các stub và spy.

Bạn sẽ sử dụng Mockito để tạo một NavigationController mô phỏng có thể xác nhận rằng phương thức điều hướng đã được gọi đúng cách.

Bước 1. Thêm phần phụ thuộc vào Gradle

  1. Thêm các phần phụ thuộc 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 – Đây là phần phụ thuộc Mockito.
  • dexmaker-mockito – Bạn phải dùng thư viện này để sử dụng Mockito trong một dự án Android. Mockito cần tạo các lớp trong thời gian chạy. Trên Android, việc này được thực hiện bằng mã byte dex. Do đó, thư viện này cho phép Mockito tạo các đối tượng trong thời gian chạy trên Android.
  • androidx.test.espresso:espresso-contrib – Thư viện này được tạo thành từ các thành phần bên ngoài (do đó có tên như vậy), chứa mã kiểm thử cho các khung hiển thị nâng cao hơn, chẳng hạn như DatePickerRecyclerView. Thư viện này cũng chứa các chế độ kiểm tra Hỗ trợ tiếp cận và lớp có tên là CountingIdlingResource sẽ được đề cập sau.

Bước 2. Tạo TasksFragmentTest

  1. Mở TasksFragment.
  2. Nhấp chuột phải vào tên lớp TasksFragment rồi chọn Generate (Tạo) rồi chọn Test (Kiểm thử). Tạo một bài kiểm thử trong nhóm tài nguyên androidTest.
  3. Sao chép mã này vào 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()
    }

}

Mã này có dạng tương tự như mã TaskDetailFragmentTest mà bạn đã viết. Thao tác này thiết lập và huỷ một FakeAndroidTestRepository. Thêm một kiểm thử điều hướng để kiểm thử rằng khi bạn nhấp vào một việc cần làm trong danh sách việc cần làm, thao tác này sẽ đưa bạn đến TaskDetailFragment chính xác.

  1. Thêm kiểm thử 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. Dùng hàm mock của Mockito để tạo một đối tượng mô phỏng.

TasksFragmentTest.kt

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

Để mô phỏng trong Mockito, hãy truyền vào lớp mà bạn muốn mô phỏng.

Tiếp theo, bạn cần liên kết NavController với mảnh. onFragment cho phép bạn gọi các phương thức trên chính mảnh đó.

  1. Tạo mô hình giả mới cho NavController của mảnh.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Thêm mã để nhấp vào mục trong RecyclerView có văn bản "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions là một phần của thư viện espresso-contrib và cho phép bạn thực hiện các thao tác Espresso trên RecyclerView.

  1. Xác minh rằng navigate đã được gọi bằng đối số chính xác.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Phương thức verify của Mockito là phương thức tạo ra đối tượng mô phỏng này. Bạn có thể xác nhận navController được mô phỏng đã gọi một phương thức cụ thể (navigate) với một tham số (actionTasksFragmentToTaskDetailFragment có mã nhận dạng "id1").

Chương trình kiểm thử hoàn chỉnh sẽ có dạng như sau:

@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. Chạy kiểm thử!

Tóm lại, để kiểm thử hoạt động điều hướng, bạn có thể:

  1. Dùng Mockito để tạo một đối tượng mô phỏng NavController.
  2. Đính kèm NavController mô phỏng đó vào mảnh.
  3. Xác minh rằng navigate được gọi bằng(các) tham số và thao tác chính xác.

Bước 3. Không bắt buộc, hãy viết clickAddTaskButton_navigateToAddEditFragment

Để xem liệu bạn có thể tự viết một kiểm thử điều hướng hay không, hãy thử thực hiện nhiệm vụ này.

  1. Viết kiểm thử clickAddTaskButton_navigateToAddEditFragment để kiểm tra xem nếu bạn nhấp vào nút hành động nổi +, bạn sẽ chuyển đến AddEditTaskFragment.

Câu trả lời ở bên dưới.

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

Nhấp vào đây để xem sự khác biệt giữa mã bạn bắt đầu và mã cuối cùng.

Để tải mã xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng lệnh git bên dưới:

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


Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP, sau đó giải nén và mở tệp đó trong Android Studio.

Tải tệp Zip xuống

Lớp học lập trình này trình bày cách thiết lập tính năng chèn phần phụ thuộc theo cách thủ công, một bộ định vị dịch vụ, cũng như cách sử dụng dữ liệu giả và đối tượng mô phỏng trong các ứng dụng Android Kotlin. Cụ thể:

  • Những gì bạn muốn kiểm thử và chiến lược kiểm thử sẽ xác định loại kiểm thử mà bạn sẽ triển khai cho ứng dụng của mình. Kiểm thử đơn vị là loại kiểm thử tập trung và nhanh chóng. Kiểm thử tích hợp xác minh hoạt động tương tác giữa các phần của chương trình. Kiểm thử toàn diện xác minh các tính năng, có độ trung thực cao nhất, thường được đo lường và có thể mất nhiều thời gian hơn để chạy.
  • Cấu trúc của ứng dụng ảnh hưởng đến mức độ khó khăn khi kiểm thử.
  • TDD (Phát triển dựa trên kiểm thử) là một chiến lược mà bạn viết các bài kiểm thử trước, sau đó tạo tính năng để vượt qua các bài kiểm thử.
  • Để tách biệt các phần của ứng dụng để kiểm thử, bạn có thể sử dụng kiểm thử kép. Kiểm thử kép là một phiên bản của lớp được tạo riêng cho mục đích kiểm thử. Ví dụ: bạn giả mạo việc lấy dữ liệu từ cơ sở dữ liệu hoặc Internet.
  • Sử dụng tính năng chèn phần phụ thuộc để thay thế một lớp thực bằng một lớp kiểm thử, chẳng hạn như một kho lưu trữ hoặc một lớp mạng.
  • Sử dụng kiểm thử đo lường (androidTest) để chạy các thành phần giao diện người dùng.
  • Khi không thể sử dụng tính năng chèn phần phụ thuộc của hàm khởi tạo (ví dụ: để chạy một mảnh), bạn thường có thể sử dụng một công cụ định vị dịch vụ. Mẫu Công cụ định vị dịch vụ là một giải pháp thay thế cho tính năng Chèn phần phụ thuộc. Việc này liên quan đến việc tạo một lớp singleton có tên là "Service Locator" (Công cụ định vị dịch vụ), có mục đích là cung cấp các phần phụ thuộc cho cả mã thông thường và mã kiểm thử.

Khoá học của Udacity:

Tài liệu dành cho nhà phát triển Android:

Video:

Khác:

Để biết đường liên kết đến các lớp học lập trình khác trong khoá học này, hãy xem trang đích của các lớp học lập trình trong khoá học Kiến thức nâng cao về cách tạo ứng dụng Android bằng Kotlin.