Giới thiệu về quy trình kiểm tra phụ thuộc và chèn phụ thuộc

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:

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:

Tải tệp zip 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Đã 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ự:

Đ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: com.example.android.architecture.blueprints.todoapp

.addedittask

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.

.data

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

.statistics

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

.taskdetail

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.

.tasks

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.

.util

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:

  1. Trước tiên, bạn sẽ kiểm tra đơn vị kho lưu trữ.
  2. 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ịthử nghiệm tích hợp mô hình chế độ xem.
  3. 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.
  4. 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ư LocalTaskDataSourceRemoteTaskDataSource, để hoạt động. Một cách nói khác là LocalTaskDataSourceRemoteTaskDataSourcecác phần phụ thuộc của DefaultTaskRepository.

Vì vậy, mọi phương thức trong DefaultTaskRepository đều gọi các phương thức trên 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ụ: StubTaskRepository có thể được lập trình để trả về một số tổ hợp tác vụ nhất định từ getTasks.

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ó NoOpTaskRepository, thì chỉ cần triển khai TaskRepository mà không cần mã trong bất kỳ phương thức nào.

đ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 SpyTaskRepository, thì thử nghiệm này có thể theo dõi số lần phương thức addTask được gọi.

Để 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ạokhá.

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

  1. Trong tập hợp nguồn test, hãy nhấp chuột phải vào New -> Package.

  1. Hãy tạo một gói dữ liệu với gói nguồn bên trong.
  2. Tạo một lớp mới có tên là FakeDataSource trong gói data/source.

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

Để có thể sử dụng lớp 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à TasksLocalDataSourceTasksRemoteDataSource.

  1. 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 { ... }
  1. Cho phép FakeDataSource triển khai TasksDataSource:
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.

  1. Dùng trình đơn khắc phục nhanh và chọn Triển khai thành viên.


  1. 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.
  1. Thay đổi hàm dựng FakeDataSource để tạo một var có tên là tasks – một MutableList<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, deleteAllTaskssaveTask.

Viết phiên bản giả mạo của các phương thức sau:

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

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ế TasksRemoteDataSourceTasksLocalDataSource, nhưng chỉ cần thay thế trong các thử nghiệm. Cả TasksRemoteDataSourceTasksLocalDataSource đề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 taskLocalDataSourcetasksRemoteDataSource 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

  1. Thay đổi hàm dựng của DefaultTaskRepository từ việc lấy Application 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 }
  1. 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.
  2. 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
  1. 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.

  1. 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.
  2. Làm theo lời nhắc để tạo DefaultTasksRepositoryTest trong nhóm nguồn thử nghiệm.
  3. Ở đầ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 }
  1. 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 cho DefaultTasksRepository 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.

  1. Tạo một phương thức có tên là createRepository và chú thích phương thức đó bằng @Before.
  2. Tạo các nguồn dữ liệu giả mạo bằng danh sách remoteTaskslocalTasks.
  3. 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!

  1. 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ọi getTasks bằng true (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.

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

  1. 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.
  2. Quay lại DefaultTasksRepositoryTest của bạn, thêm runBlockingTest để 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))
    }

}
  1. 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, TasksLocalDataSourceFakeDataSource 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.

  1. Mở DefaultTasksRepositorynhấ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.

  1. Chọn Trích xuất để tách tệp.

  1. Trong cửa sổ Trích xuất giao diện, hãy đổi tên giao diện thành TasksRepository.
  2. 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ư.


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

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

  1. 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ện TasksRepository.

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.

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

  1. Trong FakeTestRepository, hãy thêm cả biến LinkedHashMap đạ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:

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

  1. Thêm phương thức addTasks (lấy trong vararg nhiệm vụ), thêm mỗi phương thức vào HashMap 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.

  1. Mở TasksViewModel.
  2. Thay đổi hàm dựng của TasksViewModel để nhận hàm TasksRepository 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.

  1. Ở cuối tệp TasksViewModel, bên ngoài lớp học, hãy thêm một TasksViewModelFactory để đảm bảo văn bản thuần túy TasksRepository.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


Đâ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.

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

  1. Mở để mở TasksViewModelTest.
  2. Hãy thêm thuộc tính FakeTestRepository vào TasksViewModelTest.

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Cập nhật phương thức setupViewModel để tạo FakeTestRepository bằng 3 việc cần làm, sau đó tạo tasksViewModel 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)
        
    }
  1. 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).
  2. 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 TaskDetailFragmentTaskDetailViewModel. Thao tác này sẽ chuẩn bị mã để viết bài kiểm tra TaskDetail tiếp theo.

  1. Mở TaskDetailViewModel.
  2. 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 }
  1. Ở cuối tệp TaskDetailViewModel, hãy thêm TaskDetailViewModelFactory 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)
}
  1. 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))
}
  1. 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 TasksFragmentTasksDetailFragment.

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

  1. 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 AndroidX
  • kotlinx-coroutines-test—Thư viện thử nghiệm coroutine
  • androidx.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.

  1. Mở taskdetail.TaskDetailFragment.
  2. 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ồn test).

  1. 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).

  1. 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ột FragmentScenario, 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.

  1. Đâ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ị.
  2. 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:

  1. 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".
  2. Tái cấu trúc mã của bạn để khi bạn cần một kho lưu trữ, hãy sử dụng Bộ định vị dịch vụ.
  3. 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.

  1. Tạo tệp ServiceLocator.kt ở cấp cao nhất trong tập hợp nguồn chính.
  2. Xác định object có tên là ServiceLocator.
  3. Tạo các biến thực thể databaserepository, rồi đặt cả hai thành null.
  4. 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:

  1. provideTasksRepository – Cung cấp một kho lưu trữ hiện có hoặc tạo một kho lưu trữ mới. Phương thức này phải là synchronized trên this để tránh 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ữ.
  2. createTasksRepository – Mã để tạo kho lưu trữ mới. Sẽ gọi createTaskLocalDataSource và tạo TasksRemoteDataSource mới.
  3. createTaskLocalDataSource – Mã để tạo một nguồn dữ liệu cục bộ mới. Sẽ gọi cho createDataBase.
  4. 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.

  1. Ở cấp cao nhất trong hệ thống phân cấp gói, hãy mở TodoApplication và tạo một val cho kho lưu trữ của bạn và chỉ định kho lưu trữ đó bằng cách sử dụng ServiceLocator.provideTaskRepository.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

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.

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

  1. Mở TaskDetailFragement rồi tìm cuộc gọi cho getRepository ở đầu lớp học.
  2. 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)
}
  1. 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)
    }
  1. Đối với StatisticsViewModelAddEditTaskViewModel, 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

  1. 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 testandroidTest. 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.

  1. 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.
  2. Tạo một lớp mới trong gói nguồn này, gọi là FakeAndroidTestRepository.kt.
  3. 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.

  1. Mở ServiceLocator.kt.
  2. Đánh dấu phương thức setter cho tasksRepository@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).

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.

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

ServiceLocator.kt

private val lock = Any()
  1. Thêm một phương thức 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.

  1. Mở TaskDetailFragmentTest.
  2. Khai báo một biến lateinit TasksRepository.
  3. 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()
    }
  1. Giới thiệu nội dung hàm của activeTaskDetails_DisplayedInUi() trong runBlockingTest.
  2. Lưu activeTask trong kho lưu trữ trước khi chạy mảnh.
repository.saveTask(activeTask)

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)

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

    }

}
  1. 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):

  1. 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.
  2. 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ếpThang 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:

1. Phương pháp Espresso tĩnh

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

  1. Mở TaskDetailFragmentTest.kt.
  2. 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
  1. 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ụng withId, đồng thời kiểm tra để xác định giao diện của trang chi tiết.
  2. 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.

  1. 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
}
  1. Đang xem xét thử nghiệm trước, hãy hoàn tất thử nghiệm này.
  2. 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

  1. Thêm các phần phụ thuộc gradle.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core – Đây là phần phụ thuộc 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ư DatePickerRecyclerView. 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

  1. Mở TasksFragment.
  2. 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.
  3. Sao chép mã này vào TasksFragmentTest.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

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

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Mã này 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.

  1. 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)
        
    }
  1. 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 đó.

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

RecyclerViewActions là một phần 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.

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

Phương 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")
    )
}
  1. Chạy kiểm thử!

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

  1. Sử dụng Mockito để tạo bản mô phỏng NavController.
  2. Đính kèm NavController đã mô phỏng vào mảnh.
  3. 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.

  1. 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 đến AddEditTaskFragment.

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.

Tải tệp zip xuống

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:

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.