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:
- Ngôn ngữ lập trình Kotlin
- Các khái niệm kiểm thử được đề cập trong lớp học lập trình đầu tiên: Viết và chạy kiểm thử đơn vị trên Android, sử dụng JUnit, Hamcrest, AndroidX test, Robolectric, cũng như Kiểm thử LiveData
- Các thư viện Android Jetpack cốt lõi sau đây:
ViewModel,LiveDatavà Thành phần điều hướng - Cấu trúc ứng dụng, tuân theo mẫu trong Hướng dẫn về cấu trúc ứng dụng và Lớp học lập trình Kiến thức cơ bản về Android
- Kiến thức cơ bản về coroutine trên Android
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:
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 và Đã 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ự:
- Lớp học lập trình Room with a View
- Lớp học lập trình đào tạo về kiến thức cơ bản về Kotlin cho Android
- Lớp học lập trình nâng cao về Android
- Mẫu Android Sunflower
- Khoá đào tạo Phát triển ứng dụng Android bằng Kotlin trên Udacity
Đ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: | |
| 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. |
| 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ữ. |
| 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ê. |
| 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. |
| 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ụ. |
| 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:
- Trước tiên, bạn sẽ kiểm thử đơn vị kho lưu trữ.
- 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ị và kiểm thử tích hợp mô hình hiển thị.
- 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.
- 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ư LocalTaskDataSource và RemoteTaskDataSource, để hoạt động. Một cách khác để nói điều này là LocalTaskDataSource và RemoteTaskDataSource là cá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ề. |
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ó |
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 |
Để 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ả và Đố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 LocalDataSource và RemoteDataSource.
- 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).

- Tạo một gói data có một gói source bên trong.
- Tạo một lớp mới có tên là
FakeDataSourcetrong 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à TasksLocalDataSource và TasksRemoteDataSource.

- 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 { ... }- Yêu cầu
FakeDataSourcetriển khaiTasksDataSource:
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.
- Sử dụng trình đơn sửa lỗi nhanh rồi chọn Implement members (Triển khai thành viên).

- 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
DefaultTasksRepositorymà 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ử.
- Thay đổi hàm khởi tạo
FakeDataSourceđể tạo mộtvarcó tên làtasks. Đây là mộtMutableList<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, deleteAllTasks và saveTask 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:
- Write
getTasks: Nếutaskskhông phải lànull, hãy trả về kết quảSuccess. Nếutaskslànull, hãy trả về kết quảError. - Viết
deleteAllTasks: xoá danh sách việc cần làm có thể thay đổi. - 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ế TasksRemoteDataSource và TasksLocalDataSource, nhưng chỉ trong các kiểm thử. Cả TasksRemoteDataSource và TasksLocalDataSource đề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 taskLocalDataSource và tasksRemoteDataSource 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
- Thay đổi hàm dựng
DefaultTaskRepositorytừ việc nhậnApplicationthà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 }- 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. - Đồ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- 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.
- Nhấp chuột phải vào tên lớp
DefaultTasksRepositoryrồi chọn Generate (Tạo), sau đó chọn Test (Kiểm thử). - Làm theo lời nhắc để tạo
DefaultTasksRepositoryTesttrong nhóm tài nguyên kiểm thử. - Ở đầu lớp
DefaultTasksRepositoryTestmớ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 }- 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 choDefaultTasksRepositorymà 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: DefaultTasksRepositoryTạ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.
- Tạo một hàm có tên là
createRepositoryrồi chú thích hàm đó bằng@Before. - Khởi tạo nguồn dữ liệu giả bằng cách sử dụng danh sách
remoteTasksvàlocalTasks. - Khởi tạo
tasksRepositorybằ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!
- Viết một kiểm thử cho phương thức
getTaskscủa kho lưu trữ. Kiểm tra để đảm bảo rằng khi bạn gọigetTasksbằngtrue(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ử.
- 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.
- Thêm
@ExperimentalCoroutinesApiphí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. - Quay lại
DefaultTasksRepositoryTest, hãy thêmrunBlockingTestđể 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))
}
}- Chạy kiểm thử
getTasks_requestsAllTasksFromRemoteDataSourcemớ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, TasksLocalDataSource và FakeDataSource, 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.
- Mở
DefaultTasksRepositoryrồ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).

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

- 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. - 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ư.

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

Và DefaultTasksRepository hiện triển khai TasksRepository.
- 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.
- Trong tập hợp nguồn test, trong data/source, hãy tạo tệp và lớp Kotlin
FakeTestRepository.ktrồi mở rộng từ giao diệnTasksRepository.
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.
- 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).
- 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.
- Trong
FakeTestRepository, hãy thêm cả biếnLinkedHashMapđại diện cho danh sách việc cần làm hiện tại vàMutableLiveDatacho 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:
getTasks– Phương thức này sẽ lấytasksServiceDatavà chuyển thành một danh sách bằng cách sử dụngtasksServiceData.values.toList(), sau đó trả về danh sách đó dưới dạng kết quảSuccess.refreshTasks– Cập nhật giá trị củaobservableTasksthành giá trị dogetTasks()trả về.observeTasks– Tạo một coroutine bằngrunBlockingvà chạyrefreshTasks, 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ụ.
- Thêm phương thức
addTasks. Phương thức này sẽ nhận mộtvararggồm các việc cần làm, thêm từng việc vàoHashMap, 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.
- Mở
TasksViewModel. - Thay đổi hàm khởi tạo của
TasksViewModelđể nhậnTasksRepositorythay 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.
- Ở cuối tệp
TasksViewModel, bên ngoài lớp, hãy thêm mộtTasksViewModelFactorynhậnTasksRepositorythuầ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ị.
- 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))
}- 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ả.
- Mở
TasksViewModelTest. - Thêm thuộc tính
FakeTestRepositoryvàoTasksViewModelTest.
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
}- Cập nhật phương thức
setupViewModelđể tạo mộtFakeTestRepositorycó 3 tác vụ, sau đó tạotasksViewModelbằ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)
}- Vì không còn dùng mã AndroidX Test
ApplicationProvider.getApplicationContextnữa, bạn cũng có thể xoá chú thích@RunWith(AndroidJUnit4::class). - 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 TaskDetailFragment và TaskDetailViewModel. 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.
- Mở
TaskDetailViewModel. - 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 }- Ở cuối tệp
TaskDetailViewModelbên ngoài lớp, hãy thêm mộtTaskDetailViewModelFactory.
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)
}- 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))
}- 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 TasksFragment và TasksDetailFragment.
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
- 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 Corekotlinx-coroutines-test– Thư viện kiểm thử coroutineandroidx.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.
- Mở
taskdetail.TaskDetailFragment. - 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êntest).

- 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ị@SmallTestvà 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).
- 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
launchFragmentInContainertạo mộtFragmentScenario, 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.
- Đâ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.
- 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:
- 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".
- 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.
- 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.
- Tạo tệp ServiceLocator.kt ở cấp cao nhất của nhóm tài nguyên chính.
- Xác định một
objectcó tên làServiceLocator. - Tạo các biến thực thể
databasevàrepository, đồng thời đặt cả hai thànhnull. - Chú thích kho lưu trữ bằng
@Volatilevì 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:
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àsynchronizedtrênthisđể 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.createTasksRepository– Mã để tạo một kho lưu trữ mới. Sẽ gọicreateTaskLocalDataSourcevà tạo mộtTasksRemoteDataSourcemới.createTaskLocalDataSource– Mã để tạo một nguồn dữ liệu cục bộ mới. Sẽ gọi sốcreateDataBase.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.
- Ở cấp cao nhất của hệ phân cấp gói, hãy mở
TodoApplicationvà tạo mộtvalcho 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ằngServiceLocator.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.
- Mở
DefaultTasksRepositoryrồ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.
- Mở
TaskDetailFragementvà tìm lệnh gọi đếngetRepositoryở đầu lớp. - 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)
}- 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)
}- Đối với
StatisticsViewModelvàAddEditTaskViewModel, 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
- 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 test và androidTest. 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.
- Nhấp chuột phải vào tập hợp nguồn
androidTestrồ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 . - Tạo một lớp mới trong gói nguồn này có tên là
FakeAndroidTestRepository.kt. - 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.
- Mở
ServiceLocator.kt. - Đánh dấu phương thức thiết lập cho
tasksRepositorylà@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 setCho 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ử).
Vì 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ử.
- Thêm một biến thực thể có tên là
lockvới giá trịAny.
ServiceLocator.kt
private val lock = Any()- 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.
- Mở
TaskDetailFragmentTest. - Khai báo một biến
lateinit TasksRepository. - Thêm một phương thức thiết lập và một phương thức huỷ để thiết lập
FakeAndroidTestRepositorytrướ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()
}
- Gói phần nội dung hàm của
activeTaskDetails_DisplayedInUi()trongrunBlockingTest. - Lưu
activeTasktrong 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)
}- 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)
}
}
- 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!):
- 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.
- 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ếp và Tỷ 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:
onViewonView 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ị.
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ả perform và check 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.
- Mở
TaskDetailFragmentTest.kt. - 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- 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ụngwithId, đồng thời kiểm tra để đưa ra các khẳng định về giao diện của trang chi tiết. - 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ử.
- Tạo một bài kiểm thử mới có tên là
completedTaskDetails_DisplayedInUirồ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
}- Nhìn vào bài kiểm tra trước, hãy hoàn thành bài kiểm tra này.
- 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
- 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ưDatePickervàRecyclerView. 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àCountingIdlingResourcesẽ được đề cập sau.
Bước 2. Tạo TasksFragmentTest
- Mở
TasksFragment. - Nhấp chuột phải vào tên lớp
TasksFragmentrồ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. - 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.
- 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)
}
- Dùng hàm
mockcủ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 đó.
- Tạo mô hình giả mới cho
NavControllercủa mảnh.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}- Thêm mã để nhấp vào mục trong
RecyclerViewcó 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.
- 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")
)
}- Chạy kiểm thử!
Tóm lại, để kiểm thử hoạt động điều hướng, bạn có thể:
- Dùng Mockito để tạo một đối tượng mô phỏng
NavController. - Đính kèm
NavControllermô phỏng đó vào mảnh. - 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.
- 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 đếnAddEditTaskFragment.
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.
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:
- Hướng dẫn về cấu trúc ứng dụng
runBlockingvàrunBlockingTestFragmentScenario- Espresso
- Mockito
- JUnit4
- Thư viện kiểm thử AndroidX
- Thư viện kiểm thử cốt lõi của Bộ thành phần cấu trúc AndroidX
- Nhóm tài nguyên
- Kiểm thử qua dòng lệnh
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.




