Lớp học lập trình này nằm trong khóa học Nâng cao về Android trong Kotlin. Bạn sẽ nhận được nhiều giá trị nhất từ khóa học này nếu bạn làm việc qua các lớp học lập trình theo trình tự, nhưng bạn không bắt buộc phải làm vậy. Tất cả các lớp học lập trình đều có trên trang đích của các lớp học lập trình Android nâng cao trong Kotlin.
Giới thiệu
Lớp học thử nghiệm thứ hai này hướng đến tất cả các trường hợp thử nghiệm kép: khi nào nên sử dụng các tính năng đó trong Android và cách triển khai bằng cách sử dụng tính năng chèn phụ thuộc, mẫu Bộ định vị dịch vụ và thư viện. Để làm điều này, bạn sẽ học được cách viết:
- Kiểm tra đơn vị kho lưu trữ
- Mảnh và thử nghiệm tích hợp mô hình xem
- Các thử nghiệm điều hướng theo mảnh
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 thử nghiệm được đề cập trong lớp học lập trình đầu tiên: Viết và chạy thử nghiệm đơn vị trên Android, sử dụng JUnit, Hamcrest, AndroidX test, Robolectric cũng như Test LiveData
- Các thư viện Android Jetpack cốt lõi sau:
ViewModel
,LiveData
và Thành phần điều hướng - Cấu trúc ứng dụng, theo mẫu từ Hướng dẫn về cấu trúc ứng dụng và lớp học lập trình Android Fundamentals
- Khái niệm 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 thử nghiệm
- Cách tạo và sử dụng các nội dung thử nghiệm, cụ thể là giả và mô phỏng
- Cách sử dụng tính năng chèn phụ thuộc thủ công trên Android cho các thử nghiệm đơn vị và tích hợp
- Cách áp dụng Mẫu bộ định vị dịch vụ
- Cách kiểm tra kho lưu trữ, mảnh, mô hình chế độ xem 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 bài kiểm tra đơn vị cho một kho lưu trữ bằng cách sử dụng quy trình chèn thử nghiệm kép và phần phụ thuộc.
- Viết thử nghiệm đơn vị cho mô hình chế độ xem bằng cách sử dụng tính năng thử nghiệm kép và chèn phụ thuộc.
- Viết bài kiểm tra tích hợp cho các mảnh và mô hình chế độ xem bằng khung thử nghiệm giao diện người dùng Espresso.
- Viết các bài kiểm tra điều hướng bằng Mockito và Espresso.
Trong chuỗi 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 công việc để hoàn thành và hiển thị chúng trong danh sách. Sau đó, bạn có thể đánh dấu lời nhắc là đã hoàn thành, hay không, lọc hoặc xóa lời nhắc.
Ứng dụng này được viết bằng Kotlin, có một vài màn hình, sử dụng các thành phần Jetpack và theo cấu trúc từ Hướng dẫn về cấu trúc ứng dụng. Bằng cách tìm hiểu cách thử nghiệm ứng dụng này, bạn có thể thử nghiệm các ứng dụng sử dụng cùng 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ó 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 mã này, theo hướng dẫn dưới đây.
Bước 1: Chạy ứng dụng mẫu
Sau khi bạn 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. Nên biên dịch. Khám phá ứng dụng bằng cách làm như sau:
- Tạo một việc mới cần làm bằng nút dấu cộng thao tác nổi. Nhập tiêu đề trước, sau đó nhập thêm thông tin về việc cần làm. Lưu mật khẩu bằng kiểm tra FAB 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 bạn vừa hoàn thành, rồi xem màn hình chi tiết về việc cần làm đó để xem phần mô tả còn lại.
- Trong danh sách hoặc trên màn hình chi tiết, hãy đánh dấu vào hộp kiểm của việc cần làm đó để đặt trạng thái cho Việc cần làm.
- Quay lại màn hình việc cần làm, mở trình đơn bộ lọc rồi lọc việc cần làm theo trạng thái Đang hoạt động và Đã hoàn thành.
- Mở ngăn điều hướng và nhấp vào Thống kê.
- Quay lại màn hình tổng quan và từ trình đơn ngăn điều hướng, hãy chọn Xóa đã hoàn tất để xóa 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 được xây dựng dựa trên mẫu kiến trúc và thử nghiệm Bản vẽ kiến trúc phổ biến (sử dụng phiên bản cấu trúc phản hồi của mẫu). Ứng dụng này tuân theo cấu trúc từ Hướng dẫn về cấu trúc ứng dụng. Chế độ xem này sử dụng ViewModel với các Mảnh, kho lưu trữ và Phòng. Nếu bạn quen thuộc với bất kỳ ví dụ nào dưới đây, ứng dụng này có cấu trúc tương tự:
- Phòng có Lớp học mã
- Các lớp học đào tạo về lập trình Android Kotlin Fundamentals
- Lớp học lập trình nâng cao về Android
- Mẫu Android Sunflower
- Phát triển ứng dụng Android bằng khóa đào tạo Kotlin Udacity
Điều quan trọng là bạn phải hiểu được cấu trúc chung của ứng dụng hơn là hiểu sâu về logic ở bất kỳ lớp nào.
Dưới đây là phần tóm tắt các gói bạn sẽ tìm thấy:
Gói hàng: | |
| Thêm hoặc chỉnh sửa màn hình 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ẻ này chứa cơ sở dữ liệu, mạng và mã kho lưu trữ. |
| Màn hình thống kê: Mã lớp giao diện người dùng cho màn hình thống kê. |
| Màn hình thông tin chi tiết về việc cần làm: Mã lớp giao diện người dùng cho một việc cần làm. |
| Màn hình việc cần làm: Mã lớp giao diện người dùng cho danh sách tất cả việc cần làm. |
| Lớp tiện ích: Lớp học dùng chung được dùng trong nhiều phần của ứng dụng, ví dụ: cho bố cục làm mới khi vuốt được sử dụng trên nhiều màn hình. |
Lớp dữ liệu (.data)
Ứng dụng này bao gồm 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 hóa, trong dự án này, lớp mạng được mô phỏng chỉ bằng HashMap
có độ trễ, thay vì thực hiện các yêu cầu mạng thực.
DefaultTasksRepository
tọa độ hoặc dàn xếp giữa lớp mạng và lớp cơ sở dữ liệu, đồng thời là dữ liệu 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, .stats, .taskdetail, .tasks)
Mỗi gói lớp giao diện người dùng chứa một mảnh và mô hình chế độ xem, cùng với bất kỳ lớp nào khác cần thiết cho giao diện người dùng (chẳng hạn như bộ chuyển đổi cho danh sách tác vụ). 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. Tên này được xác định trong tệp nav_graph.xml
. Trình kích hoạt được kích hoạt trong các mô hình chế độ xem bằng cách sử dụng lớp Event
; các mô hình chế độ xem cũng xác định các đối số cần chuyển. Các mảnh quan sát Event
và thực hiện việc di chuyển 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 tra các kho lưu trữ, xem mô hình và các mảnh bằng cách sử dụng tính năng kiểm tra kép và chèn phần phụ thuộc. Trước khi tìm hiểu kỹ các nguyên tắc đó, bạn cần phải hiểu lý do sẽ hướng dẫn những gì và cách bạn viết những thử nghiệm này.
Phần này trình bày một số phương pháp thử nghiệm hay nhất nói chung, khi áp dụng cho Android.
Kim tự tháp Thử nghiệm
Khi nghĩ về một chiến lược thử nghiệm, có ba khía cạnh thử nghiệm liên quan:
- Phạm vi – Thử nghiệm chạm bao nhiêu mã? Bạn có thể chạy thử nghiệm trên một phương thức, trên toàn bộ ứng dụng hoặc ở những nơi khác.
- Tốc độ – Tốc độ chạy thử nghiệm là bao nhiêu? Tốc độ thử nghiệm có thể thay đổi từ mili giây đến vài phút.
- Mức độ trung thực: Thử nghiệm & thế giới thực tế như thế nào? Ví dụ: nếu một phần mã mà bạn đang thử nghiệm cần thực hiện yêu cầu mạng, thì mã thử nghiệm có thực sự thực hiện yêu cầu mạng này không hay mã đó có làm giả kết quả không? Nếu thử nghiệm thực sự kết nối với mạng, điều này có nghĩa là thử nghiệm có độ chân thực cao hơn. Sự đánh đổi là do thử nghiệm có thể mất nhiều thời gian hơn để chạy, có thể dẫn đến lỗi nếu mạng ngừng hoạt động hoặc có thể tốn kém nếu sử dụng.
Có sự đánh đổi vốn có giữa những khía cạnh này. Ví dụ: tốc độ và độ chân thực là sự đánh đổi – nói chung, thử nghiệm càng nhanh, độ chân thực càng thấp và ngược lại. Một cách phổ biến để phân chia các thử nghiệm tự động là thuộc ba danh mục sau:
- Bài kiểm tra đơn vị – Đây là các bài kiểm tra có trọng tâm cao chạy trên một lớp, thường là một phương thức duy nhất trong lớp đó. Nếu thử nghiệm đơn vị không thành công, bạn có thể biết chính xác vị trí mà mã nguồn vấn đề nằm trong mã. Các mã này có độ chân thực thấp vì trong thực tế, ứng dụng của bạn liên quan nhiều hơn đến việc thực thi một phương thức hoặc lớp học. Chúng đủ nhanh để chạy mỗi khi bạn thay đổi mã. Đây thường là những thử nghiệm chạy cục bộ (trong nhóm nguồn
test
). Ví dụ: Thử nghiệm các phương pháp riêng lẻ trong mô hình chế độ xem và kho lưu trữ. - Thử nghiệm tích hợp – Các thử nghiệm này kiểm tra sự tương tác của một số lớp để đảm bảo chúng hoạt động như mong đợi khi được sử dụng cùng nhau. Một cách để định cấu hình thử nghiệm tích hợp là yêu cầu thử nghiệm một tính năng duy nhất, chẳng hạn như khả năng lưu việc cần làm. Thử nghiệm có phạm vi mã lớn hơn so với thử nghiệm đơn vị, nhưng vẫn được tối ưu hóa để chạy nhanh so với có độ trung thực đầy đủ. Có thể chạy quảng cáo cục bộ hoặc thử nghiệm đo lường, tùy thuộc vào tình huống. Ví dụ: Thử nghiệm tất cả chức năng của một mảnh và cặp mô hình chế độ xem.
- Thử nghiệm khép kín (E2e) – Thử nghiệm tổ hợp các tính năng hoạt động cùng nhau. Họ thử nghiệm các phần lớn của ứng dụng, mô phỏng việc sử dụng thực tế và do đó thường diễn ra chậm. Loại quảng cáo này có độ trung thực cao nhất và cho bạn biết rằng toàn bộ ứng dụng thực sự hoạt động tốt. Nhìn chung, các thử nghiệm này sẽ là công cụ thử nghiệm (trong tập hợp nguồn
androidTest
)
Ví dụ: Khởi động toàn bộ ứng dụng và thử nghiệm một vài tính năng cùng nhau.
Tỷ lệ đề xuất của các thử nghiệm này thường được biểu thị bằng hình chóp, trong đó phần lớn các thử nghiệm là thử nghiệm đơn vị.
Cấu trúc và thử nghiệm
Khả năng thử nghiệm ứng dụng của bạn ở tất cả các cấp độ khác nhau của kim tự tháp thử nghiệm vốn luôn gắn liền với cấu trúc của ứng dụng. Ví dụ: một ứng dụng có cấu trúc cực kỳ có thể đặt tất cả logic của nó vào một phương thức. Bạn có thể viết một thử nghiệm từ đầu đến cuối cho thử nghiệm này, vì những thử nghiệm này thường có xu hướng thử nghiệm phần lớn ứng dụng, nhưng còn về những thử nghiệm viết hoặc đơn vị tích hợp thì sao? Với tất cả các mã ở một nơi, thật khó để chỉ thử nghiệm mã có liên quan đến một đơn vị hoặc một tính năng.
Một phương pháp hay hơn là chia nhỏ logic ứng dụng thành nhiều phương thức và lớp, cho phép thử nghiệm riêng từng phần. Cấu trúc là một cách để chia và sắp xếp mã, cho phép bạn thử nghiệm đơn vị và tích hợp dễ dàng hơn. Ứng dụng TO-DO mà bạn sẽ thử nghiệm theo một cấu trúc cụ thể:
Trong bài học này, bạn sẽ xem cách thử nghiệm các phần của cấu trúc nêu trên, trong trường hợp tách biệt đúng cách:
- Trước tiên, bạn sẽ kiểm tra đơn vị kho lưu trữ.
- Sau đó, bạn sẽ sử dụng một thử nghiệm kép trong mô hình chế độ xem, điều này cần thiết cho thử nghiệm đơn vị và thử nghiệm tích hợp mô hình chế độ xem.
- Tiếp theo, bạn sẽ tìm hiểu cách viết thử nghiệm tích hợp cho các mảnh và mô hình chế độ xem của chúng.
- Cuối cùng, bạn sẽ tìm hiểu cách viết các thử nghiệm tích hợp bao gồm Thành phần điều hướng.
Thử nghiệm từ đầu đến cuối sẽ được đề cập trong bài học tiếp theo.
Khi bạn viết bài kiểm tra đơn vị cho một phần của lớp (một phương thức hoặc một tập hợp phương thức nhỏ), mục tiêu của bạn là chỉ kiểm tra mã trong lớp đó.
Việc chỉ kiểm tra mã trong một lớp hoặc các lớp cụ thể có thể phức tạp. Hãy xem ví dụ. Mở lớp data.source.DefaultTaskRepository
trong nhóm 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 tra đơn vị cho lần tiếp theo.
Mục tiêu của bạn là chỉ kiểm tra mã trong lớp đó. Tuy nhiên, DefaultTaskRepository
hoạt động 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 nói khác 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 lớp nguồn dữ liệu, từ đó dẫn đến các phương thức gọi 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 và phổ biế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 pháp 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 tới updateTasksFromRemoteDataSource
). Cách này bao gồm nhiều mã hơn chỉ là mã kho lưu trữ.
Sau đây là một số lý do cụ thể khác khiến việc kiểm tra kho lưu trữ gặp 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 cả những thử nghiệm đơn giản nhất cho kho lưu trữ này. Thao tác này sẽ hiển thị các câu hỏi như " đây có phải là thử nghiệm cục bộ hoặc phương thức {/5}quot; và nếu bạn nên sử dụng Thử nghiệm AndroidX để có được môi trường Android được mô phỏ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 thậm chí không thành công, tạo ra các thử nghiệm lâu dài, không ổn định.
- Thử nghiệm của bạn có thể mất khả năng chẩn đoán mã nào bị lỗi do lỗi thử nghiệm. Các thử nghiệm của bạn có thể bắt đầu thử nghiệm mã không lưu trữ, vì vậy, ví dụ:
Đôi thử nghiệm
Giải pháp cho vấn đề này là khi bạn đang thử nghiệm kho lưu trữ, không sử dụng mã mạng thực hoặc mã cơ sở dữ liệu, mà thay vào đó hãy sử dụng thử nghiệm kép. Thử nghiệm kép là một phiên bản của lớp học được tạo riêng để thử nghiệm. Mục đích của tệp này là thay thế phiên bản thực của một lớp trong thử nghiệm. Trò chơi này đóng vai trò tương tự như cách diễn viên đóng thế là một diễn viên chuyên thực hiện các pha mạo hiểm và thay thế diễn viên thực tế cho các hành động nguy hiểm.
Dưới đây là một số loại đôi thử nghiệm:
Giả mạo | Bộ thử nghiệm này có cách triển khai và lớp học, nhưng được triển khai theo cách phù hợp cho việc thử nghiệm nhưng không phù hợp với việc sản xuất. |
Nội dung mô phỏng | Thử nghiệm kép theo dõi phương thức nào được gọi. Sau đó, phương thức này vượt qua hoặc không vượt qua được thử nghiệm tùy thuộc vào việc các phương thức của phương thức này có được gọi chính xác hay không. |
Mã giả lập | Thử nghiệm nhân đôi không bao gồm logic và chỉ trả về nội dung bạn lập trình để trả về. Ví dụ: |
Bố | Thử nghiệm kép được chuyển qua nhưng không được sử dụng, chẳng hạn như bạn chỉ cần cung cấp thử nghiệm đó dưới dạng tham số. Nếu bạn có |
điệp viên | Thử nghiệm cũng theo dõi một số thông tin bổ sung; ví dụ: nếu bạn thực hiện |
Để biết thêm thông tin về quy trình kiểm tra kép, hãy xem bài viết Thử nghiệm bồn cầu: Biết đôi thử nghiệm.
Kỹ thuật thử phổ biến nhất được dùng trong Android là giả mạo và khá.
Trong tác vụ này, bạn sẽ tạo một thử nghiệm FakeDataSource
gấp đôi để đơn vị thử nghiệm DefaultTasksRepository
được tách ra từ các nguồn dữ liệu thực tế.
Bước 1: Tạo lớp falseDataSource
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 thử nghiệm kép của một LocalDataSource
và RemoteDataSource
.
- Trong tập hợp nguồn test, hãy nhấp chuột phải vào New -> Package.
- Hãy tạo một gói dữ liệu với gói nguồn bên trong.
- 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 mới FakeDataSource
làm thử nghiệm, bạn phải thay thế được các nguồn dữ liệu khác. Các nguồn dữ liệu đó là TasksLocalDataSource
và TasksRemoteDataSource
.
- Hãy lưu ý cách cả hai cách này 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 { ... }
- Cho phép
FakeDataSource
triển khaiTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio sẽ khiếu nại rằng bạn chưa triển khai các phương thức bắt buộc cho TasksDataSource
.
- Dùng trình đơn khắc phục nhanh và chọn Triển khai thành viên.
- Chọn tất cả phương thức và nhấn OK.
Bước 3: Triển khai phương thức getTasks trong falseDataSource
FakeDataSource
là một loại thử nghiệm kép cụ thể được gọi là giả mạo. Kỹ thuật giả mạo là một thử nghiệm kép có cách thức hoạt động và triển khai lớp học, nhưng được triển khai theo cách phù hợp với việc thử nghiệm nhưng không phù hợp với việc sản xuất. "Làm việc&quo; việc triển khai có nghĩa là lớp này sẽ tạo ra các kết quả thực trong đầu vào đã cho.
Ví dụ: nguồn dữ liệu giả của bạn sẽ không kết nối mạng hoặc lưu bất kỳ nội dung nào vào cơ sở dữ liệu – thay vào đó, nguồn này sẽ chỉ sử dụng danh sách trong bộ nhớ. Phương thức này sẽ "làm việc như bạn mong đợi" trong các phương pháp đó để nhận hoặc lưu nhiệm vụ sẽ trả về kết quả mong đợi, nhưng bạn không bao giờ có thể sử dụng cách triển khai này trong sản xuất, vì nó không được lưu vào máy chủ hoặc cơ sở dữ liệu.
Một FakeDataSource
- cho phép bạn kiểm tra 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 cách triển khai "real-en" để thử nghiệm.
- Thay đổi hàm dựng
FakeDataSource
để tạo mộtvar
có tên làtasks
– mộtMutableList<Task>?
có giá trị mặc định của một danh sách có thể thay đổi.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Đây là danh sách những việc cần làm & được phép làm giả; cơ sở dữ liệu hoặc phản hồi của máy chủ. Hiện tại, mục tiêu của bạn là thử nghiệm phương thức kho lưu trữ getTasks
. Thao tác này sẽ gọi các phương thức nguồn dữ liệu\39;s getTasks
, deleteAllTasks
và saveTask
.
Viết phiên bản giả mạo của các phương thức sau:
- Viết
getTasks
: Nếutasks
khôngnull
, hãy trả về kết quảSuccess
. Nếutasks
lànull
, hãy trả về một kết quảError
. - Viết
deleteAllTasks
: xóa 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.
Các phương thức đó, được triển khai cho FakeDataSource
, trông giố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)
}
Dưới đây là các bản kê khai 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 thực tế tại địa phương và nguồn từ xa.
Trong bước này, bạn sẽ sử dụng kỹ thuật có tên là chèn phụ thuộc thủ công để có thể sử dụng thử nghiệm giả mà bạn vừa tạo.
Vấn đề chính là bạn có FakeDataSource
, nhưng cách sử dụng thử nghiệm không rõ ràng. Bạn cần thay thế TasksRemoteDataSource
và TasksLocalDataSource
, nhưng chỉ cần thay thế trong các thử nghiệm. Cả TasksRemoteDataSource
và TasksLocalDataSource
đều là phần phụ thuộc của DefaultTasksRepository
, nghĩa là DefaultTasksRepositories
yêu cầu hoặc "depends" trên các lớp này để chạy.
Hiện tại, các phần phụ thuộc được tạo bên 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 chúng được mã hóa cứng. Không có cách nào để chuyển đổi trong thử nghiệm kép của bạn.
Điều bạn muốn làm là cung cấp những nguồn dữ liệu này cho lớp thay vì mã hóa 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 phần phụ thuộc và do đó có nhiều loại chèn phần phụ thuộc khác nhau.
Tính năng chèn phụ thuộc của cấu trúc cho phép bạn hoán đổi trong thử nghiệm kép bằng cách chuyển hàm này vào hàm dựng.
Không tiêm | Chèn |
Bước 1: Sử dụng tính năng Chèn phụ thuộc hàm dựng trong DefaultTasksRepository
- Thay đổi hàm dựng của
DefaultTaskRepository
từ việc lấyApplication
sang nhận cả hai nguồn dữ liệu và trình điều phối coroutine (bạn cũng sẽ cần hoán đổi cho các thử nghiệm của mình – thông tin này được mô tả chi tiết hơn trong phần bài học thứ ba 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 đã chuyển các phần phụ thuộc vào, hãy xóa phương thức
init
. Bạn không cần phải tạo các phần phụ thuộc nữa. - Xóa cả các biến thực thể cũ. Bạn đang xác định chúng trong hàm dựng:
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 dựng 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
}
}
}
}
Bạn hiện đang sử dụng tính năng chèn phần phụ thuộc hàm dựng!
Bước 2: Sử dụng giảDataSource trong thử nghiệm của bạn
Giờ đây, mã của bạn đang sử dụng tính năng chèn phụ thuộc hàm dựng, bạn có thể sử dụng nguồn dữ liệu giả mạo để kiểm tra DefaultTasksRepository
của mình.
- Nhấp chuột phải vào tên lớp
DefaultTasksRepository
rồi chọn Tạo, sau đó chọn Kiểm tra. - Làm theo lời nhắc để tạo
DefaultTasksRepositoryTest
trong nhóm nguồn thử nghiệm. - Ở đầu lớp
DefaultTasksRepositoryTest
mới, hãy thêm các biến thành viên bên dưới để thể hiện dữ liệu trong nguồn dữ liệu giả của bạn.
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 dành cho thành viên
FakeDataSource
(một biến cho mỗi nguồn dữ liệu cho kho lưu trữ của bạn) và một biến choDefaultTasksRepository
mà bạn sẽ thử nghiệm.
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ể thử nghiệm. DefaultTasksRepository
này sẽ sử dụng thử nghiệm kép của bạn, FakeDataSource
.
- Tạo một phương thức có tên là
createRepository
và chú thích phương thức đó bằng@Before
. - Tạo các nguồn dữ liệu giả mạo bằng danh sách
remoteTasks
vàlocalTasks
. - Tạo
tasksRepository
bằng cách sử dụng hai 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ẽ giố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 bài kiểm tra DefaultTasksRepository getTasks()
Đã đến lúc viết bài kiểm tra DefaultTasksRepository
!
- Viết thử nghiệm cho phương thức
getTasks
của kho lưu trữ. Kiểm tra để đảm bảo rằng khi bạn gọigetTasks
bằngtrue
(nghĩa là trình thu thập dữ liệu sẽ tải lại từ nguồn dữ liệu từ xa), hệ thống sẽ trả về dữ liệu từ nguồn dữ liệu từ xa (chứ không phải 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 RunBlockTest
Lỗi coroutine dự kiến sẽ xảy ra vì getTasks
là hàm suspend
và bạn cần chạy coroutine để gọi hàm đó. Để làm được việc đó, bạn cần có phạm vi coroutine. Để giải quyết lỗi này, bạn cần phải thêm một số phần phụ thuộc gradle để xử lý việc chạy coroutine trong các thử nghiệm.
- Thêm các phần phụ thuộc bắt buộc để kiểm tra coroutine vào nguồn thử nghiệm bằng cách dùng
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
Đừng quên đồng bộ hóa!
kotlinx-coroutines-test
là thư viện thử nghiệm coroutine, dành riêng cho việc kiểm tra coroutine. Để chạy thử nghiệm, hãy dùng hàm runBlockingTest
. Đây là một hàm do thư viện thử nghiệm coroutine cung cấp. Sự kiện này lấy một khối mã rồi chạy khối mã này trong ngữ cảnh coroutine đặc biệt, chạy đồng bộ và ngay lập tức, nghĩa là các hành động sẽ xảy ra theo thứ tự xác định. Về cơ bản, điều này giúp coroutine của bạn chạy giống như các coroutine không phải, vì vậy, bạn nên dùng mã này để kiểm tra mã.
Hãy dùng runBlockingTest
trong các lớp kiểm tra 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 tra coroutine trong lớp học lập trình tiếp theo trong loạt bài này.
- Thêm
@ExperimentalCoroutinesApi
vào phía trên lớp học. Điều này thể hiện rằng bạn biết mình đang sử dụng API coroutine thử nghiệm (runBlockingTest
) trong lớp này. Nếu không có cookie này, bạn sẽ nhận được cảnh báo. - Quay lại
DefaultTasksRepositoryTest
của bạn, thêmrunBlockingTest
để thử nghiệm trong toàn bộ thử nghiệm dưới dạng "block" của mã
Thử nghiệm cuối cùng này trông giố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 thử nghiệm
getTasks_requestsAllTasksFromRemoteDataSource
mới và xác nhận rằng thử nghiệm hoạt động và lỗi đã biến mất!
Bạn vừa tìm hiểu cách kiểm tra đơn vị kho lưu trữ. Trong các bước tiếp theo này, bạn sẽ sử dụng lại tính năng chèn phụ thuộc và tạo một thử nghiệm kép — lần này là để trình bày cách viết thử nghiệm đơn vị và tích hợp cho các mô hình chế độ xem của bạn.
Bạn chỉ nên kiểm tra lớp học hoặc phương thức thử nghiệm mà bạn quan tâm. Việc này được gọi là thử nghiệm trong tính năng tách biệt, trong đó bạn tách biệt rõ ràng "unit" và chỉ kiểm tra mã thuộc đơn vị đó.
Vì vậy, TasksViewModelTest
chỉ nên kiểm tra mã TasksViewModel
chứ không nên kiểm tra trong cơ sở dữ liệu, mạng hoặc các lớp kho lưu trữ. Do đó, đối với mô hình chế độ xem, giống như bạn vừa làm với kho lưu trữ của mình, bạn sẽ tạo một kho lưu trữ giả và áp dụng kho lưu trữ phụ thuộc để sử dụng kho lưu trữ đó trong các thử nghiệm của mình.
Trong tác vụ này, bạn áp dụng tính năng chèn phần phụ thuộc để xem các mô hình.
Bước 1. Tạo giao diện kho lưu trữ Tasks
Bước đầu tiên để sử dụng tính năng chèn phần phụ thuộc hàm dựng là tạo một giao diện chung được chia sẻ giữa lớp giả và lớp thực.
Điều này trông như thế nào trong thực tế? Hãy xem TasksRemoteDataSource
, TasksLocalDataSource
và FakeDataSource
và nhận thấy rằng tất cả đều có cùng một giao diện: TasksDataSource
. Điều này cho phép bạn nói trong hàm dựng của DefaultTasksRepository
mà bạn lấy trong TasksDataSource
.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
Chính vì vậy, chúng tôi có thể thay đổi thiết bị này trong FakeDataSource
của bạn!
Tiếp theo, hãy tạo giao diện cho DefaultTasksRepository
, giống như bạn đã làm đối với các nguồn dữ liệu. Sự kiện này cần bao gồm tất cả các phương thức công khai (nền tảng API công khai) của DefaultTasksRepository
.
- Mở
DefaultTasksRepository
và nhấp chuột phải vào tên lớp học. Sau đó, chọn Tái cấu trúc -> Trích xuất -> Giao diện.
- Chọn Trích xuất để tách tệp.
- Trong cửa sổ Trích xuất giao diện, hãy đổi tên giao diện thành
TasksRepository
. - Trong phần Thành viên tạo giao diện, hãy chọn tất cả thành viên ngoại trừ hai thành viên đồng hành và phương thức riêng tư.
- Nhấp vào Tái cấu trúc. Giao diện
TasksRepository
mới sẽ xuất hiện trong gói dữ liệu/nguồn .
Giờ đây, DefaultTasksRepository
sẽ triển khai TasksRepository
.
- Chạy ứng dụng của bạn (không phải thử nghiệm) để đảm bảo mọi thứ vẫn hoạt động bình thường.
Bước 2. Tạo kho lưu trữ giả mạo
Giờ đây, khi đã có giao diện, bạn có thể tạo thử nghiệm DefaultTaskRepository
hai lần.
- Trong tập hợp nguồn test, trong data/source, hãy tạo tệp Kotlin và lớp
FakeTestRepository.kt
, đồng thời mở rộng từ giao diệnTasksRepository
.
giảTestRepository.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 qua lỗi cho đến khi bạn thấy trình đơn đề xuất, rồi nhấp và chọn Triển khai thành viên.
- Chọn tất cả phương thức và nhấn OK.
Bước 3. Triển khai các phương thức Kiểm tra giả mạo
Giờ đây, bạn có một lớp FakeTestRepository
có "not triển khai" các phương pháp. 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ì xử lý một hoạt động dàn xếp 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
của bạn không cần phải sử dụng FakeDataSource
hoặc bất kỳ phương thức nào khác; chỉ cần trả về kết quả giả mạo thực tế đã cho. Bạn sẽ dùng LinkedHashMap
để lưu trữ danh sách việc cần làm và MutableLiveData
cho những việc cần làm có thể quan sát được.
- Trong
FakeTestRepository
, hãy thêm cả biếnLinkedHashMap
đại diện cho danh sách các tác vụ hiện tại vàMutableLiveData
cho các tác vụ có thể quan sát.
giảTestRepository.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 pháp sau:
getTasks
—Phương thức này sẽ lấytasksServiceData
và biến giá trị đó thành một danh sách bằngtasksServiceData.values.toList()
rồi trả về kết quả đó dưới dạng kết quảSuccess
.refreshTasks
—Cập nhật giá trị củaobservableTasks
thành giá trị trả vềgetTasks()
.observeTasks
– Tạo coroutine bằng cách sử dụngrunBlocking
và chạyrefreshTasks
, sau đó trả vềobservableTasks
.
Dưới đây là mã cho các phương thức đó.
giảTestRepository.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 phương thức thử nghiệm để thêm Tasks
Khi thử nghiệm, bạn nên có một số Tasks
trong kho lưu trữ của mình. Bạn có thể gọi saveTask
nhiều lần, nhưng để làm việc này dễ dàng hơn, hãy thêm một phương thức trợ giúp dành riêng cho thử nghiệm cho phép bạn thêm việc cần làm.
- Thêm phương thức
addTasks
(lấy trongvararg
nhiệm vụ), thêm mỗi phương thức vàoHashMap
rồi làm mới việc cần làm.
giảTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
Lúc này, bạn có một kho lưu trữ giả để thử nghiệm bằng một vài phương pháp chính được triển khai. Tiếp theo, hãy sử dụng mã này trong các thử nghiệm của bạn!
Trong nhiệm vụ này, bạn sử dụng một lớp giả bên trong ViewModel
. Hãy dùng tính năng chèn phần phụ thuộc hàm dựng để lấy 2 nguồn dữ liệu thông qua tính năng chèn phần phụ thuộc hàm dựng bằng cách thêm biến TasksRepository
vào hàm dựng TasksViewModel
#39;
Quá trình này hơi khác với các mô hình chế độ xem 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 đại biểu thuộc tính của viewModel's
để tạo mô hình chế độ xem. Để thay đổi cách xây dựng mô hình chế độ xem, bạn cần thêm và sử dụng ViewModelProvider.Factory
. Nếu chưa biết rõ về ViewModelProvider.Factory
, bạn có thể tìm hiểu thêm về thuộc tính này tại đây.
Bước 1. Tạo và sử dụng ViewModelFactory trong TasksViewModel
Bạn bắt đầu với việc cập nhật các lớp học và thử nghiệm liên quan đến màn hình Tasks
.
- Mở
TasksViewModel
. - Thay đổi hàm dựng của
TasksViewModel
để nhận hàmTasksRepository
thay vì tạo hàm 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ì bạn đã thay đổi hàm dựng, nên bây giờ bạn cần dùng một trạng thái ban đầu để tạo TasksViewModel
. Đặt lớp gốc về cùng một tệp với TasksViewModel
, nhưng bạn cũng có thể đặt lớp này vào tệp của riêng mình.
- Ở cuối tệp
TasksViewModel
, bên ngoài lớp học, hãy thêm mộtTasksViewModelFactory
để đảm bảo văn bản thuần túyTasksRepository
.
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 để thay đổi cách xây dựng ViewModel
. Giờ bạn đã có nhà máy, hãy sử dụng nó bất cứ khi nào bạn tạo mô hình chế độ xem.
- Hãy cập nhật
TasksFragment
để sử dụng trạng thái ban đầu.
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 của bạn và đảm bảo mọi thứ vẫn hoạt động!
Bước 2. Sử dụng falseTestRepository bên trong TasksViewModelTest
Bây giờ, thay vì sử dụng kho lưu trữ thực trong thử nghiệm mô hình chế độ xem, bạn có thể sử dụng kho lưu trữ giả mạo.
- Mở để mở
TasksViewModelTest
. - Hãy thêm thuộc tính
FakeTestRepository
và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ạoFakeTestRepository
bằng 3 việc cần làm, sau đó tạotasksViewModel
với 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ì bạn không còn sử dụng mã Thử nghiệm AndroidX
ApplicationProvider.getApplicationContext
nữa, bạn cũng có thể xóa chú thích@RunWith(AndroidJUnit4::class)
. - Chạy thử nghiệm và đảm bảo tất cả thử nghiệm đều hoạt động!
Bằng cách sử dụng tính năng chèn phần phụ thuộc hàm dựng, bạn hiện đã xóa DefaultTasksRepository
dưới dạng phần phụ thuộc và thay thế phần tử này bằng FakeTestRepository
của bạn trong các thử nghiệm.
Bước 3. Cập nhật cả TaskDetail Fragment và ViewModel
Thực hiện các thay đổi giống hệt nhau cho TaskDetailFragment
và TaskDetailViewModel
. Thao tác này sẽ chuẩn bị mã để viết bài kiểm tra TaskDetail
tiếp theo.
- Mở
TaskDetailViewModel
. - Cập nhật hàm dựng:
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
TaskDetailViewModel
, hãy thêmTaskDetailViewModelFactory
bên ngoài lớp.
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)
}
- Hãy cập nhật
TasksFragment
để sử dụng trạng thái ban đầu.
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 rằng mọi thứ đang 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 thử nghiệm tích hợp để kiểm tra các hoạt động tương tác với mảnh và mô hình xem. Bạn sẽ tìm hiểu xem mã mô hình chế độ xem có cập nhật phù hợp giao diện người dùng của bạn hay không. Để làm điều này, bạn dùng
- mẫu ServiceLocator
- thư viện Espresso và Mockito
Thử nghiệm tích hợp thử nghiệm hoạt động tương tác của một số lớp để đảm bảo hoạt động như mong đợi khi được sử dụng cùng nhau. Bạn có thể chạy thử nghiệm này trên máy (test
nguồn) hoặc thử nghiệm đo lường (androidTest
nguồn).
Trong trường hợp của bạn, bạn sẽ thực hiện từng thử nghiệm và viết các bài kiểm tra tích hợp cho mảnh và mô hình chế độ xem để kiểm tra 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 sau đây trong gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
Những 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 tra cơ bản.androidx.test:core
—Thư viện thử nghiệm Core AndroidXkotlinx-coroutines-test
—Thư viện thử nghiệm coroutineandroidx.fragment:fragment-testing
—Thư viện thử nghiệm AndroidX để tạo các mảnh trong thử nghiệm 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 tập hợp nguồn androidTest
của mình, hãy sử dụng androidTestImplementation
để thêm các thư viện đó dưới dạng phần phụ thuộc.
Bước 2. Tạo một lớp TaskDetailFragmentTest
TaskDetailFragment
hiển thị thông tin về một việc cần làm.
Bạn sẽ bắt đầu bằng cách viết một thử nghiệm mảnh cho TaskDetailFragment
vì thẻ này có chức năng khá cơ bản so với các mảnh khác.
- Mở
taskdetail.TaskDetailFragment
. - Tạo thử nghiệm cho
TaskDetailFragment
, như bạn đã làm trước đó. Chấp nhận các lựa chọn mặc định và đặt lựa chọn đó vào tập hợp nguồn androidTest (KHÔNG phải tập hợp nguồntest
).
- Hãy thêm các chú thích sau vào lớp
TaskDetailFragmentTest
.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
Mục đích của các chú thích này là:
@MediumTest
– Đánh dấu thử nghiệm là thử nghiệm đơn vị &thời gian chạy trung bình; so với thử nghiệm đơn vị tích hợp (so với thử nghiệm@SmallTest
và thử nghiệm toàn diện lớn@LargeTest
). Điều này giúp bạn nhóm và chọn kích thước thử nghiệm để chạy.@RunWith(AndroidJUnit4::class)
– Dùng trong lớp bất kỳ bằng AndroidX Test.
Bước 3. Khởi chạy một mảnh từ thử nghiệm
Trong tác vụ này, bạn sẽ khởi chạy TaskDetailFragment
bằng thư viện Thử nghiệm AndroidX. FragmentScenario
là một lớp từ Thử nghiệm AndroidX bao bọc xung quanh một mảnh và cung cấp cho bạn quyền kiểm soát trực tiếp đối với vòng đời của mảnh để kiểm tra. Để viết các thử nghiệm cho mảnh, bạn tạo một FragmentScenario
cho mảnh mà bạn đang thử nghiệm (TaskDetailFragment
).
- Sao chép thử nghiệm 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 chuyển vào mảnh. - Hàm
launchFragmentInContainer
tạo mộtFragmentScenario
, trong đó có gói này và một giao diện.
Đây chưa phải là thử nghiệm đã hoàn tất vì nó không xác nhận bất kỳ điều gì. Hiện tại, hãy chạy thử nghiệm và quan sát điều gì sẽ xảy ra.
- Đây là một thử nghiệm đ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ị.
- Chạy thử nghiệm.
Có một số vấn đề sẽ xảy ra.
- Trước tiên, vì đây là thử nghiệm đo lường, nên thử nghiệm sẽ chạy trên một thiết bị thực (nếu có kết nối mạng) hoặc trình mô phỏng.
- Thao tác này sẽ khởi chạy mảnh.
- Hãy lưu ý rằng nó không di chuyển qua bất kỳ mảnh nào khác hoặc có bất kỳ trình đơn nào liên quan đến hoạt động – mà chỉ chỉ là mảnh đó.
Cuối cùng, hãy xem xét kỹ và nhận thấy rằng mảnh nói là "Không có dữ liệu" vì mảnh không tải thành công dữ liệu việc cần làm.
Cả hai thử nghiệm của bạn đều cần phải tải TaskDetailFragment
(mà bạn đã hoàn thành) và xác nhận rằng dữ liệu đã được tải đúng cách. Tại sao không có dữ liệu? Lý do là bạn đã tạo một việc cần làm nhưng không lưu việc đó 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 số cách để thay thế kho lưu trữ thực bằng kho lưu trữ giả mạo cho mảnh của bạn. Bạn sẽ làm điều này tiếp theo!
Trong tác vụ này, bạn sẽ cung cấp kho lưu trữ giả cho mảnh bằng ServiceLocator
. Nhờ đó, bạn có thể viết mảnh và xem các thử nghiệm tích hợp mô hình.
Bạn không thể sử dụng tính năng chèn phụ thuộc hàm dựng tại đây, như bạn đã làm trước đây, khi bạn cần cung cấp phần phụ thuộc vào mô hình chế độ xem hoặc kho lưu trữ. Tính năng chèn phần phụ thuộc hàm dựng yêu cầu bạn tạo lớp. Mảnh và hoạt động là ví dụ về các lớp mà bạn không tạo và thường không có quyền truy cập vào hàm dựng.
Vì bạn không xây dựng mảnh, nên bạn không thể sử dụng tính năng chèn phần phụ thuộc hàm dựng để chuyển đổi thử nghiệm kho lưu trữ (FakeTestRepository
) thành mảnh. Thay vào đó, hãy sử dụng mẫu Bộ định vị dịch vụ. Mẫu Bộ định vị dịch vụ là một lựa chọn thay thế cho tính năng Chèn phụ thuộc. Phương thức này bao gồm việc tạo một lớp singleton có tên là "Service Locator" 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 tra. Trong mã ứng dụng thông thường (nhóm nguồn main
), tất cả các phần phụ thuộc này đều là phần phụ thuộc thông thường trong ứng dụng. Đối với các thử nghiệm, bạn sửa đổi Bộ định vị dịch vụ để cung cấp các phiên bản phụ thuộc thử nghiệm.
Không sử dụng bộ định vị dịch vụ | Sử dụng bộ định vị dịch vụ |
Đối với ứng dụng lớp học lập trình này, hãy làm như sau:
- Tạo một lớp Bộ định vị dịch vụ để có thể tạo và lưu trữ kho lưu trữ. Theo mặc định, hàm này tạo một kho lưu trữ "normal".
- 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 Bộ định vị dịch vụ.
- Trong lớp kiểm tra, hãy gọi một phương thức trên Bộ định vị dịch vụ để hoán đổi kho lưu trữ "quot;normal" với bản thử nghiệm của bạn.
Bước 1. Tạo Bộ định vị dịch vụ
Hãy tạo một lớp ServiceLocator
. Mã này sẽ nằm trong nguồn chính được đặt trong phần còn lại của mã ứng dụng vì mã ứng dụng chính được sử dụng.
Lưu ý: ServiceLocator
là một singleton, vì vậy, hãy dùng từ khóa object
Kotlin cho lớp.
- Tạo tệp ServiceLocator.kt ở cấp cao nhất trong tập hợp nguồn chính.
- Xác định
object
có tên làServiceLocator
. - Tạo các biến thực thể
database
vàrepository
, rồi đặt cả hai thànhnull
. - Chú thích kho lưu trữ bằng
@Volatile
vì kho lưu trữ này có thể được nhiều luồng sử dụng (@Volatile
được giải thích chi tiết tại đây).
Mã của bạn phải trông như bên dưới.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
Hiện tại, việc duy nhất mà ServiceLocator
cần làm là biết cách trả lại một TasksRepository
. Thao tác này sẽ trả về một DefaultTasksRepository
có sẵn hoặc tạo và trả lại 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àsynchronized
trênthis
để tránh trường hợp có nhiều chuỗi đang chạy, vô tình tạo hai phiên bản kho lưu trữ.createTasksRepository
– Mã để tạo kho lưu trữ mới. Sẽ gọicreateTaskLocalDataSource
và tạoTasksRemoteDataSource
mới.createTaskLocalDataSource
– Mã để tạo một nguồn dữ liệu cục bộ mới. Sẽ gọi chocreateDataBase
.createDataBase
– Mã để tạo cơ sở dữ liệu mới.
Dưới đây là mã hoàn tất.
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ẽ cần thay đổi mã ứng dụng chính (không phải thử nghiệm) để tạo kho lưu trữ ở cùng một nơi, ServiceLocator
của bạn.
Điều quan trọng là bạn chỉ cần tạo một phiên bản của lớp kho lưu trữ. Để đảm bảo điều này, bạn sẽ sử dụng bộ định vị Dịch vụ trong lớp Ứng dụng của tôi.
- Ở cấp cao nhất trong hệ thống phân cấp gói, hãy mở
TodoApplication
và tạo mộtval
cho kho lưu trữ của bạn và chỉ định kho lưu trữ đó bằng cách sử dụ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())
}
}
Bây giờ khi bạn đã tạo kho lưu trữ trong ứng dụng, bạn có thể xóa phương thức getRepository
cũ trong DefaultTasksRepository
.
- Mở
DefaultTasksRepository
và xóa đối tượng companion.
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
}
}
}
}
Ở mọi nơi bạn sử dụng getRepository
, hãy dùng ứng dụng taskRepository
. Điều này đảm bảo rằng thay vì trực tiếp tạo 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ở
TaskDetailFragement
rồi tìm cuộc gọi chogetRepository
ở đầu lớp học. - Thay thế cuộc gọi này bằng lệnh gọi nhận 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ự như vậy 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
StatisticsViewModel
vàAddEditTaskViewModel
, hãy cập nhật mã lấy được 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 thử nghiệm)!
Vì bạn chỉ tái cấu trúc, nên ứng dụng sẽ chạy tương tự mà không có vấn đề gì.
Bước 3. Tạo kho giả mạo AndroidTestRepository
Bạn đã có FakeTestRepository
trong nhóm nguồn thử nghiệm. Theo mặc định, bạn không thể chia sẻ các lớp thử nghiệm giữa các nhóm nguồn test
và androidTest
. Vì vậy, bạn cần tạo một lớp FakeTestRepository
trùng lặp trong nhóm nguồn androidTest
và gọi lớp đó là FakeAndroidTestRepository
.
- Nhấp chuột phải vào nhóm nguồn
androidTest
và tạo gói dữ liệu. Nhấp chuột phải một lần nữa rồi tạo một gói nguồn. - Tạo một lớp mới trong gói nguồn này, gọi là
FakeAndroidTestRepository.kt
. - Sao chép mã sau vào lớp học đó.
falseAndroidTestRepository.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ị bộ định tuyến dịch vụ để thử nghiệm
Đã đến lúc sử dụng ServiceLocator
để chuyển đổi trong thử nghiệm tăng gấp đôi khi thử nghiệm. Để làm việc đó, bạn cần thêm một mã vào mã ServiceLocator
.
- Mở
ServiceLocator.kt
. - Đánh dấu phương thức setter cho
tasksRepository
là@VisibleForTesting
. Chú thích này là để thể hiện rằng lý do của phương thức setter là công khai là do thử nghiệm.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Dù bạn chạy thử nghiệm một mình hay trong một nhóm thử nghiệm, các thử nghiệm của bạn đều phải giống hệt nhau. Điều này có nghĩa là các thử nghiệm của bạn không được hoạt động phụ thuộc lẫn nhau (nghĩa là tránh việc chia sẻ đối tượng giữa các thử nghiệm).
Vì ServiceLocator
là một singleton, nên nó có khả năng vô tình được chia sẻ giữa các thử nghiệm. Để tránh điều 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 thử nghiệm.
- 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()
- Thêm một phương thức thử nghiệm cụ thể có tên là
resetRepository
. Phương thức này sẽ xóa cơ sở dữ liệu và đặt cả kho lưu trữ và cơ sở dữ liệu thành giá trị null.
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}
Bước 5. Sử dụng Bộ định vị dịch vụ
Trong bước này, bạn sử dụng ServiceLocator
.
- Mở
TaskDetailFragmentTest
. - Khai báo một biến
lateinit TasksRepository
. - Thêm thông tin thiết lập và phương pháp phân tích để thiết lập
FakeAndroidTestRepository
trước mỗi thử nghiệm và dọn dẹp sau mỗi thử nghiệm.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Giới thiệu nội dung hàm của
activeTaskDetails_DisplayedInUi()
trongrunBlockingTest
. - Lưu
activeTask
trong kho lưu trữ trước khi chạy mảnh.
repository.saveTask(activeTask)
Thử nghiệm cuối cùng 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ú thích cả lớp bằng
@ExperimentalCoroutinesApi
.
Khi hoàn tất, mã sẽ trông như thế này.
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 thử nghiệm
activeTaskDetails_DisplayedInUi()
.
Cũng giống như trước, bạn sẽ thấy mảnh, ngoại trừ thời gian này vì bạn thiết lập đúng kho lưu trữ nên bây giờ Google sẽ hiển thị thông tin việc cần làm.
Trong bước này, bạn sẽ sử dụng thư viện thử nghiệm giao diện người dùng Espresso để hoàn tất thử nghiệm tích hợp đầu tiên của mình. Bạn đã định cấu hình mã để có thể thêm thử nghiệm xác nhận cho giao diện người dùng của mình. Để làm được việc đó, bạn sẽ sử dụng thư viện thử nghiệm Espresso.
Espresso giúp bạn:
- Tương tác với chế độ xem, chẳng hạn như nhấp vào các nút, trượt thanh hoặc cuộn xuống màn hình.
- Xác nhận rằng một số chế độ xem nhất định đang ở trên màn hình hoặc ở một trạng thái nhất định (chẳng hạn như văn bản cụ thể hoặc hộp đánh dấu được chọn, v.v.).
Bước 1. Lưu ý đến phần phụ thuộc Gradle
Theo mặc định, bạn đã có phần phụ thuộc này của Espresso vì chúng đã có trong 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
– Theo mặc định, phần phụ thuộc này của Espresso có sẵn khi bạn tạo một dự án Android mới. Báo cáo này chứa mã thử nghiệm cơ bản cho hầu hết các chế độ xem và hành động đối với các chế độ xem đó.
Bước 2. Tắt hiệu ứng động
Thử nghiệm Espresso chạy trên thiết bị thực và do đó, về bản chất là thử nghiệm đo lường. Một vấn đề phát sinh là hoạt ảnh: Nếu một hoạt ảnh bị trễ và bạn cố gắng kiểm tra xem một lượt xem có xuất hiện trên màn hình hay không, nhưng hoạt ảnh đó vẫn hoạt động, thì Espresso có thể vô tình không kiểm tra được. Điều này có thể khiến thử nghiệm Espresso không hoạt động.
Đối với thử nghiệm giao diện người dùng Espresso, cách tốt nhất là tắt tính năng ảnh động (cũng như thử nghiệm sẽ chạy nhanh hơn):
- Trên thiết bị thử nghiệm, hãy chuyển đến phần Cài đặt và gt; Tùy chọn cho nhà phát triển.
- Tắt 3 chế độ cài đặt này: Tỷ lệ ảnh động của cửa sổ, Tỷ lệ ảnh động chuyển tiếp và Thang thời lượng của trình tạo hoạt ảnh.
Bước 3. Xem thử nghiệm Espresso
Trước khi bạn viết bài kiểm tra 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 tìm chế độ xem hộp đánh dấu có mã là task_detail_complete_checkbox
, nhấp vào chế độ xem đó rồi xác nhận rằng chế độ xem đó được chọn.
Phần lớn các câu lệnh Espresso bao gồm 4 phần:
onView
onView
là ví dụ về một phương thức Espresso tĩnh bắt đầu một câu lệnh Espresso. onView
là một trong những tùy chọn phổ biến nhất, nhưng có những tùy chọn khác, chẳng hạn như onData
.
2. ViewMatcher
withId(R.id.task_detail_title_text)
withId
là ví dụ về một ViewMatcher
nhận được chế độ xem theo mã. Bạn có thể tìm các kiểu khớp chế độ xem khác trong tài liệu.
3. ViewAction
perform(click())
Phương thức perform
sử dụng ViewAction
. ViewAction
là việc có thể làm đối với chế độ xem, ví dụ như ở đây, người dùng sẽ nhấp vào chế độ xem đó.
4. Xem xác nhận
check(matches(isChecked()))
check
mất ViewAssertion
. ViewAssertion
kiểm tra hoặc xác nhận điều gì đó về chế độ xem. ViewAssertion
phổ biến nhất mà bạn sẽ sử dụng là câu nhận định matches
. Để hoàn tất phần xác nhận, 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 perform
và check
trong câu lệnh Espresso. Bạn có thể dùng các câu lệnh để xác nhận bằng check
hoặc chỉ dùng ViewAction
bằng perform
.
- Mở
TaskDetailFragmentTest.kt
. - Cập nhật thử nghiệm
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())))
}
Dưới đây là các bản kê khai 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 thứ sau bình luận
// THEN
đều sử dụng cà phê Espresso. Hãy kiểm tra cấu trúc thử nghiệm và việc sử dụngwithId
, đồng thời kiểm tra để xác định giao diện của trang chi tiết. - Chạy thử nghiệm và xác nhận rằng thử nghiệm vượt qua.
Bước 4. Không bắt buộc, Viết thử nghiệm Espresso của riêng bạn
Giờ hãy tự mình thử nghiệm.
- Tạo thử nghiệm mới có tên là
completedTaskDetails_DisplayedInUi
và sao chép mã bộ xương 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
}
- Đang xem xét thử nghiệm trước, hãy hoàn tất thử nghiệm này.
- Chạy và xác nhận bài kiểm tra.
completedTaskDetails_DisplayedInUi
hoàn tất sẽ trông giố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 tra Thành phần điều hướng, sử dụng loại kiểm tra khác được gọi là mô phỏng và thư viện thử nghiệm Mockito.
Trong lớp học lập trình này, bạn đã sử dụng thử nghiệm kép có tên là giả mạo. Giả mạo là một trong nhiều loại thử nghiệm kép. Bạn nên dùng thử nghiệm nào để thử nghiệm Thành phần điều hướng?
Hãy suy nghĩ về cách điều hướng xảy ra. Hãy tưởng tượng khi nhấn vào một trong các việc cần làm trong TasksFragment
để chuyển đến màn hình thông tin chi tiết về việc cần làm.
Đây là mã của TasksFragment
được chuyển đến màn hình thông tin chi tiết về việc cần làm khi được nhấn.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
Điều hướng xảy ra do lệnh gọi đến phương thức navigate
. Nếu bạn cần viết tuyên bố xác nhận, không có cách nào đơn giản để kiểm tra xem bạn đã chuyển đến TaskDetailFragment
hay chưa. Di chuyển là một hành động 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 chạy TaskDetailFragment
.
Bạn có thể xác nhận rằng phương thức navigate
được gọi bằng thông số hành động chính xác. Đây chính là chức năng của thử nghiệm bản mô phỏng, cho phép kiểm tra xem các phương thức cụ thể có được gọi hay không.
Mockito là khung thử nghiệm giúp tăng gấp đôi. Mặc dù từ mô phỏng được sử dụng trong API và tên, nhưng không chỉ để mô phỏng. Thuốc cũng có thể làm dính vết dẹt và xoắn.
Bạn sẽ sử dụng Mockito để tạo mô hình NavigationController
có thể xác nhận rằng phương thức điều hướng đã được gọi chính xác.
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 của Mockito.dexmaker-mockito
– Bạn cần sử 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 vào thời gian chạy. Trên Android, việc này được thực hiện bằng mã byte dex. Vì vậy, thư viện này cho phép Mockito tạo đối tượng trong thời gian chạy trên Android.androidx.test.espresso:espresso-contrib
– Thư viện này bao gồm các phần đóng góp bên ngoài (do đó chứa tên) chứa mã thử nghiệm cho các chế độ xem nâng cao hơn, chẳng hạn nhưDatePicker
vàRecyclerView
. Mục này cũng chứa lớp kiểm tra Hỗ trợ tiếp cận và lớp có tên làCountingIdlingResource
. Phần này sẽ được đề cập sau.
Bước 2. Tạo TasksFragmentTest
- Mở
TasksFragment
. - Nhấp chuột phải vào tên lớp
TasksFragment
rồi chọn Tạo rồi chọn Kiểm tra. Tạo một thử nghiệm trong tập hợp nguồ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 giống với mã TaskDetailFragmentTest
mà bạn viết. Thiết lập và chia nhỏ FakeAndroidTestRepository
. Thêm phương pháp kiểm tra điều hướng để kiểm tra rằng khi nhấp vào một việc cần làm trong danh sách việc cần làm, bạn sẽ được chuyển đến đúng TaskDetailFragment
.
- Thêm thử nghiệm
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)
}
- Sử dụng hàm
mock
của Mockito\39; để tạo một bản mô phỏng.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
Để mô phỏng trong Mockito, hãy chuyển lớp học bạn muốn mô phỏng.
Tiếp theo, bạn cần liên kết NavController
của mình 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 mới cho mảnh
NavController
.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- 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 trong thư viện espresso-contrib
và cho phép bạn thực hiện các thao tác trên 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 pháp verify
của Mockito& gồm có:
Quá trình kiểm tra hoàn tất sẽ có các bước 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 tra điều hướng, bạn có thể:
- Sử dụng Mockito để tạo bản mô phỏng
NavController
. - Đính kèm
NavController
đã mô phỏng vào mảnh. - Xác minh rằng tính năng điều hướng được gọi bằng(các) thông số và hành động chính xác.
Bước 3. Không bắt buộc, viết ClickAddTaskButton_navigateToAddEditEdit
Để xem bạn có thể tự viết bài kiểm tra điều hướng hay không, hãy thử nhiệm vụ này.
- Viết bài kiểm tra
clickAddTaskButton_navigateToAddEditFragment
để kiểm tra xem bạn có nhấp vào + FAB không, sau đó chuyển đếnAddEditTaskFragment
.
Dưới đây là câu trả lờ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 xuống mã cho lớp học lập trình đã hoàn thành, 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
Hoặc bạn có thể tải kho lưu trữ xuống dưới dạng tệp Zip, giải nén và mở tệp đó trong Android Studio.
Lớp học lập trình này đề cập đến cách thiết lập tính năng chèn phụ thuộc thủ công, công cụ định vị dịch vụ và cách sử dụng ứng dụng giả mạo trong ứng dụng Android Kotlin. Cụ thể, bạn nên làm như sau:
- Những nội dung bạn muốn thử nghiệm và chiến lược thử nghiệm sẽ xác định những loại thử nghiệm mà bạn sẽ triển khai cho ứng dụng của mình. Thử nghiệm đơn vị tập trung và nhanh chóng. Thử nghiệm 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. Thử nghiệm hai đầu xác minh các tính năng, có độ chân 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ó thử nghiệm.
- TDD hoặc Phát triển theo hướng thử nghiệm là chiến lược mà trong đó bạn viết các thử nghiệm trước tiên, sau đó tạo tính năng để vượt qua các thử nghiệm.
- Để tách biệt các phần của ứng dụng cho mục đích thử nghiệm, bạn có thể sử dụng các phiên bản thử nghiệm. Thử nghiệm kép là một phiên bản của lớp học được tạo riêng để thử nghiệm. Ví dụ: bạn giả mạo tải dữ liệu từ cơ sở dữ liệu hoặc Internet.
- Dùng tính năng chèn phụ thuộc để thay thế một lớp thực bằng một lớp kiểm tra, ví dụ: một kho lưu trữ hoặc một lớp mạng.
- Dùng chế độ kiểm tra tích hợp (
androidTest
) để chạy các thành phần giao diện người dùng. - Khi bạn không thể sử dụng tính năng chèn phụ thuộc hàm dựng, ví dụ: để chạy một mảnh, bạn thường có thể sử dụng bộ định vị dịch vụ. Mẫu Bộ định vị dịch vụ là một lựa chọn thay thế cho tính năng Chèn phụ thuộc. Phương thức này bao gồm việc tạo một lớp singleton có tên là "Service Locator" 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 tra.
Khóa học từ 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
runBlocking
vàrunBlockingTest
FragmentScenario
- Espresso
- Bản minh họa
- JUnit 4
- Thư viện thử nghiệm AndroidX
- Thư viện thử nghiệm cốt lõi của các thành phần cấu trúc AndroidX
- Nhóm nguồn
- Kiểm tra từ dòng lệnh
Video:
Các tài liệu khác:
Để xem đường liên kết đến các lớp học lập trình khác trong khóa học này, hãy xem trang đích Nâng cao cho Android trong Kotlin.