Viết mã kiểm thử đơn vị cho ViewModel

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

1. Trước khi bắt đầu

Lớp học lập trình này hướng dẫn bạn cách viết các bài kiểm thử đơn vị để kiểm thử thành phần ViewModel. Bạn sẽ thêm các bài kiểm thử đơn vị cho ứng dụng trò chơi Unscramble (Xếp từ). Ứng dụng Unscramble là một trò chơi đố vui, trong đó người dùng phải đoán một từ đã được xáo trộn và kiếm điểm bằng việc đoán chính xác từ đó. Hình sau đây cho thấy bản xem trước của ứng dụng:

ecb509065f9993b1.gif

Trong lớp học lập trình Viết kiểm thử tự động, bạn đã tìm hiểu về khái niệm và tầm quan trọng của các kiểm thử tự động. Bạn cũng đã tìm hiểu cách triển khai các kiểm thử đơn vị.

Bạn đã tìm hiểu:

  • Kiểm thử tự động là mã xác minh độ chính xác của một đoạn mã khác.
  • Kiểm thử là một phần quan trọng trong quá trình phát triển ứng dụng. Bằng cách chạy các kiểm thử nhất quán với ứng dụng của mình, bạn có thể xác minh hành vi chức năng và khả năng hữu dụng của ứng dụng trước khi phát hành chính thức.
  • Với các bài kiểm thử đơn vị, bạn có thể kiểm thử các hàm, lớp và thuộc tính.
  • Kiểm thử đơn vị cục bộ được thực thi trên máy trạm của bạn, tức là các kiểm thử này chạy trong môi trường phát triển mà không cần trình mô phỏng hay thiết bị Android. Nói cách khác, kiểm thử cục bộ sẽ chạy trên máy tính của bạn.

Trước khi tiếp tục, hãy nhớ hoàn thành các lớp học lập trình Viết kiểm thử tự độngViewModel và Trạng thái trong Compose.

Điều kiện tiên quyết

  • Kiến thức về Kotlin, bao gồm các hàm, hàm lambda và các thành phần kết hợp không có trạng thái
  • Kiến thức cơ bản về cách xây dựng bố cục trong Jetpack Compose.
  • Kiến thức cơ bản về Material Design.
  • Kiến thức cơ bản về cách triển khai ViewModel

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

  • Cách thêm phần phụ thuộc cho các kiểm thử đơn vị trong tệp build.gradle của mô-đun ứng dụng
  • Cách tạo chiến lược kiểm thử để triển khai các kiểm thử đơn vị
  • Cách viết các mã kiểm thử đơn vị bằng JUnit4 và hiểu vòng đời của bản sao kiểm thử
  • Cách chạy, phân tích và cải thiện mức độ bao phủ của mã

Sản phẩm bạn sẽ tạo ra

  • Các kiểm thử đơn vị cho ứng dụng trò chơi Unscramble

Những gì bạn cần

  • Phiên bản mới nhất của Android Studio

Lấy mã khởi động

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

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

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git
$ cd basic-android-kotlin-compose-training-unscramble
$ git checkout viewmodel

Bạn có thể xem mã này trong kho lưu trữ GitHub Unscramble.

2. Tổng quan về mã khởi động

Ở Bài 2, bạn đã tìm hiểu cách đặt mã kiểm thử đơn vị trong nhóm tài nguyên kiểm thử thuộc thư mục src như trong hình sau.

86aead386aae572a.png

Mã khởi động có hai tệp:

  • WordsData.kt: Tệp này chứa danh sách các từ cần dùng để kiểm thử và một hàm trợ giúp getUnscrambledWord() để lấy từ không bị xáo trộn trong các từ bị xáo trộn. Bạn không cần sửa đổi tệp này.

3. Thêm các phần phụ thuộc kiểm thử

Trong lớp học lập trình này, bạn sẽ sử dụng khung JUnit để viết các mã kiểm thử đơn vị. Để sử dụng khung này, bạn cần thêm nó vào phần phụ thuộc trong tệp build.gradle của mô-đun ứng dụng.

Bạn sẽ sử dụng cấu hình implementation để chỉ định các phần phụ thuộc mà ứng dụng yêu cầu. Ví dụ: để sử dụng thư viện ViewModel trong ứng dụng, bạn phải thêm phần phụ thuộc vào androidx.lifecycle:lifecycle-viewmodel-compose, như minh hoạ trong đoạn mã sau:

dependencies {

    ...
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0"
}

Bạn hiện đã có thể sử dụng thư viện này trong mã nguồn của ứng dụng, và Android Studio sẽ giúp thêm thư viện này vào Tệp gói ứng dụng (APK) đã tạo. Tuy nhiên, bạn sẽ không muốn mã kiểm thử đơn vị là một phần của tệp APK. Mã kiểm thử không thêm bất kỳ chức năng nào mà người dùng sẽ sử dụng, đồng thời mã này cũng ảnh hưởng đến kích thước APK. Tương tự như với các phần phụ thuộc bắt buộc của mã kiểm thử; bạn nên tách biệt chúng. Để thực hiện điều này, bạn hãy sử dụng cấu hình testImplementation để cho biết cấu hình áp dụng cho mã nguồn kiểm thử cục bộ chứ không phải mã ứng dụng.

Để thêm một phần phụ thuộc vào dự án, hãy chỉ định cấu hình cho phần phụ thuộc (chẳng hạn như implementation hoặc testImplementation) trong khối phần phụ thuộc của tệp build.gradle. Mỗi cấu hình của phần phụ thuộc cung cấp cho Gradle các hướng dẫn khác nhau về cách sử dụng phần phụ thuộc.

Cách thêm một phần phụ thuộc:

  1. Mở tệp build.gradle của mô-đun app, nằm ở thư mục app trong ngăn Project (Dự án).

4a06ab2a560a096.png

  1. Bên trong tệp, hãy cuộn xuống cho đến khi bạn thấy khối dependencies {}. Sử dụng cấu hình testImplementation để thêm phần phụ thuộc vào junit.
plugins {
    ...
}

android {
    ...
}

dependencies {
    ...
    testImplementation 'junit:junit:4.13.2'
}
  1. Trong thanh thông báo ở đầu tệp build.gradle, hãy nhấp vào Sync Now (Đồng bộ hoá ngay) để cho phép hoàn tất các thao tác nhập và tạo như trong ảnh chụp màn hình sau:

def0a9820607a08b.png

Tuyệt vời! Bạn đã thêm thành công các phần phụ thuộc kiểm thử vào ứng dụng. Giờ thì bạn đã sẵn sàng để thêm một số kiểm thử đơn vị.

4. Chiến lược kiểm thử

Một chiến lược kiểm thử hiệu quả xoay quanh việc hỗ trợ nhiều đường dẫn và ranh giới (giá trị biên) mã của bạn. Ở cấp độ cơ bản nhất, bạn có thể phân loại các bài kiểm thử theo ba trường hợp: đường dẫn thành công, đường dẫn lỗi và trường hợp kiểm thử giá trị biên (ranh giới).

  • Đường dẫn thành công: Các bài kiểm thử đường dẫn thành công (còn được gọi là kiểm thử đường dẫn hạnh phúc) tập trung vào việc kiểm thử chức năng của một luồng tích cực. Luồng dương là luồng không có điều kiện ngoại lệ hoặc lỗi. So với đường dẫn lỗi và các trường hợp kiểm thử giá trị biên (ranh giới), bạn có thể dễ dàng tạo một danh sách đầy đủ các trường hợp cho đường dẫn thành công, vì các đường dẫn này tập trung vào hành vi dự kiến cho ứng dụng của bạn.

Một ví dụ về đường dẫn thành công trong ứng dụng Unscramble là việc cập nhật chính xác điểm số, số từ và từ được xáo trộn khi người dùng nhập một từ đúng rồi nhấp vào nút Submit (Gửi).

  • Đường dẫn lỗi: Các bài kiểm thử đường dẫn lỗi tập trung vào việc kiểm thử chức năng của một luồng âm – tức là để kiểm tra cách ứng dụng phản hồi các điều kiện lỗi hoặc hoạt động đầu vào của người dùng không hợp lệ. Rất khó để xác định tất cả các luồng lỗi có thể xảy ra vì có khá nhiều kết quả có thể xảy ra khi không đạt được hành vi mong muốn.

Bạn nên liệt kê mọi đường dẫn lỗi có thể xảy ra, viết mã kiểm thử cho những đường dẫn đó và duy trì việc phát triển các bài kiểm thử đơn vị khi bạn phát hiện ra các trường hợp nào khác.

Một ví dụ về đường dẫn lỗi trong ứng dụng Unscramble là người dùng nhập từ sai và nhấp vào nút Submit (Gửi), điều này khiến thông báo lỗi xuất hiện cũng như điểm và số từ sẽ không được cập nhật.

  • Trường hợp kiểm thử giá trị biên (ranh giới): Trường hợp này tập trung vào việc kiểm thử các điều kiện của giá trị biên (ranh giới) trong ứng dụng. Trong ứng dụng Unscramble, kiểm thử giá trị biên (ranh giới) là các hoạt động kiểm tra trạng thái giao diện người dùng khi ứng dụng tải và trạng thái giao diện người dùng sau khi người dùng chơi hết số từ tối đa.

Việc tạo các trường hợp kiểm thử xoay quanh những danh mục này có thể đóng vai trò là nguyên tắc cho kế hoạch kiểm thử.

Tạo các bài kiểm thử

Một kiểm thử đơn vị phù hợp thường có bốn thuộc tính sau:

  • Có trọng tâm: Tập trung vào việc kiểm thử một đơn vị, chẳng hạn như một đoạn mã. Đoạn mã này thường là một lớp hoặc một phương thức. Bài kiểm thử nên giới hạn và tập trung vào việc xác thực độ chính xác của từng đoạn mã thay vì nhiều đoạn mã cùng lúc.
  • Dễ hiểu: Khi bạn đọc, mã kiểm thử này phải đơn giản và dễ hiểu. Chỉ cần lướt qua, nhà phát triển đã có thể hiểu ngay ý định đằng sau kiểm thử đó.
  • Có tính xác định: Kết quả phải luôn đạt hoặc không đạt một cách nhất quán. Khi chạy các bài kiểm thử nhiều lần mà không phải thay đổi mã, bài kiểm thử vẫn trả về cùng một kết quả. Bài kiểm thử không được có kết quả không ổn định, có lỗi trong một thực thể và truyền vào một thực thể khác, mặc dù mã không bị sửa đổi.
  • Độc lập: Không yêu cầu một sự tương tác hoặc thiết lập nào của con người và chạy một cách độc lập.

Đường dẫn thành công

Để viết một kiểm thử đơn vị cho đường dẫn thành công, bạn cần xác nhận rằng, với một thực thể của GameViewModel đã được khởi tạo, khi phương thức updateUserGuess() được gọi với từ dự đoán chính xác, theo sau là một lệnh gọi đến phương thức checkUserGuess(), thì:

  • Dự đoán chính xác được truyền đến phương thức updateUserGuess().
  • Phương thức checkUserGuess() được gọi.
  • Giá trị của trạng thái scoreisGuessedWordWrong sẽ cập nhật chính xác.

Hãy hoàn thành các bước sau để tạo kiểm thử:

  1. Tạo một gói mới com.example.android.unscramble.ui.test trong nhóm tài nguyên kiểm thử và thêm tệp GameViewModelTest.kt như trong ảnh chụp màn hình sau:

fee23bfea18e890a.png

Để viết kiểm thử đơn vị cho lớp GameViewModel, bạn cần có một bản sao của lớp này để có thể gọi các phương thức của lớp này và xác minh trạng thái.

  1. Trong phần nội dung của lớp GameViewModelTest, hãy khai báo thuộc tính viewModel và gán một thực thể của lớp GameViewModel cho thuộc tính đó.
class GameViewModelTest {
    private val viewModel = GameViewModel()
}
  1. Để viết kiểm thử đơn vị cho đường dẫn thành công, hãy tạo hàm gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() và chú thích hàm đó bằng chú thích @Test.
class GameViewModelTest {
    private val viewModel = GameViewModel()

    @Test
    fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset()  {
   }
}

Để truyền một từ chính xác của người chơi đến phương thức viewModel.updateUserGuess(), bạn cần lấy từ đúng chưa được xáo trộn trong các từ đã được xáo trộn thuộc GameUiState. Để thực hiện việc này, trước tiên, hãy lấy trạng thái giao diện người dùng hiện tại của trò chơi.

  1. Trong phần nội dung hàm, hãy tạo một biến currentGameUiState và chỉ định biến đó cho viewModel.uiState.value.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
}
  1. Để lấy người chơi đoán đúng, hãy sử dụng hàm getUnscrambleWord(), hàm này nhận currentGameUiState.currentScrambledWord làm đối số và trả về từ không được xáo trộn. Lưu trữ giá trị được trả về này trong một biến mới chỉ có thể đọc có tên là unScrambledWord, và chỉ định giá trị mà hàm getUnscrambledWord() trả về.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

}
  1. Để xác minh xem từ được đoán có đúng hay không, hãy thêm một lệnh gọi vào phương thức viewModel.updateUserGuess() và truyền biến correctPlayerWord vào làm đối số. Sau đó, hãy thêm một lệnh gọi vào phương thức viewModel.checkUserGuess() để xác minh dự đoán.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

    viewModel.updateUserGuess(correctPlayerWord)
    viewModel.checkUserGuess()
}

Giờ đây, bạn đã sẵn sàng xác nhận là trạng thái trò chơi đúng như bạn mong đợi.

  1. Lấy thực thể của lớp GameUiState từ giá trị của thuộc tính viewModel.uiState và lưu trữ lớp đó trong biến currentGameUiState.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    viewModel.updateUserGuess(correctPlayerWord)
    viewModel.checkUserGuess()

    currentGameUiState = viewModel.uiState.value
}
  1. Để kiểm tra xem từ đoán có phải là từ đúng và điểm số đã được cập nhật hay chưa, hãy sử dụng hàm assertFalse() để xác minh thuộc tính currentGameUiState.isGuessedWordWrongfalse, và sử dụng hàm assertEquals() để xác minh giá trị của thuộc tính currentGameUiState.score bằng 20.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    viewModel.updateUserGuess(correctPlayerWord)
    viewModel.checkUserGuess()

    currentGameUiState = viewModel.uiState.value
    // Assert that checkUserGuess() method updates isGuessedWordWrong is updated correctly.
    assertFalse(currentGameUiState.isGuessedWordWrong)
    // Assert that score is updated correctly.
    assertEquals(20, currentGameUiState.score)
}
  1. Để giá trị 20 có thể đọc và sử dụng lại được, hãy tạo một đối tượng đồng hành và gán 20 cho một hằng số private có tên là SCORE_AFTER_FIRST_CORRECT_ANSWER. Cập nhật kiểm thử bằng hằng số mới tạo.
class GameViewModelTest {
    ...
    @Test
    fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
        ...
        // Assert that score is updated correctly.
        assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
    }

    companion object {
        private const val SCORE_AFTER_FIRST_CORRECT_ANSWER = SCORE_INCREASE
    }
}
  1. Chạy kiểm thử.

Kiểm thử phải đạt (thành công) vì tất cả các câu nhận định đều hợp lệ, như trong ảnh chụp màn hình sau đây:

c6bd246467737a32.png

Đường dẫn lỗi

Để viết một kiểm thử đơn vị cho đường dẫn lỗi, bạn cần xác nhận là khi một từ không chính xác được truyền làm đối số cho phương thức viewModel.updateUserGuess() và phương thức viewModel.checkUserGuess() được gọi, thì những điều sau sẽ xảy ra:

  • Giá trị của thuộc tính currentGameUiState.score vẫn không thay đổi.
  • Giá trị của thuộc tính currentGameUiState.isGuessedWordWrong được thiết lập thành true do phỏng đoán không chính xác.

Hãy hoàn thành các bước sau để tạo kiểm thử:

  1. Trong phần nội dung của lớp GameViewModelTest, hãy tạo một hàm gameViewModel_IncorrectGuess_ErrorFlagSet() và chú thích hàm này bằng chú thích @Test.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {

}
  1. Xác định biến incorrectPlayerWord và chỉ định giá trị "and" cho biến đó. Giá trị này không tồn tại trong danh sách các từ.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
    // Given an incorrect word as input
    val incorrectPlayerWord = "and"
}
  1. Thêm lệnh gọi vào phương thức viewModel.updateUserGuess() và truyền biến incorrectPlayerWord làm đối số.
  2. Thêm một lệnh gọi vào phương thức viewModel.checkUserGuess() để xác minh dự đoán.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
    // Given an incorrect word as input
    val incorrectPlayerWord = "and"

    viewModel.updateUserGuess(incorrectPlayerWord)
    viewModel.checkUserGuess()
}
  1. Thêm biến currentGameUiState và chỉ định giá trị của trạng thái viewModel.uiState.value cho biến đó.
  2. Dùng các hàm xác nhận để xác nhận giá trị của thuộc tính currentGameUiState.score0, và giá trị của thuộc tính currentGameUiState.isGuessedWordWrong được đặt thành true.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
    // Given an incorrect word as input
    val incorrectPlayerWord = "and"

    viewModel.updateUserGuess(incorrectPlayerWord)
    viewModel.checkUserGuess()

    val currentGameUiState = viewModel.uiState.value
    // Assert that score is unchanged
    assertEquals(0, currentGameUiState.score)
    // Assert that checkUserGuess() method updates isGuessedWordWrong correctly
    assertTrue(currentGameUiState.isGuessedWordWrong)
}
  1. Chạy kiểm thử để xác nhận là kiểm thử đạt.

Trường hợp kiểm thử giá trị biên (ranh giới)

Để kiểm thử trạng thái ban đầu của giao diện người dùng, bạn cần viết một mã kiểm thử đơn vị cho lớp GameViewModel. Kiểm thử này phải xác nhận là khi khởi chạy GameViewModel thì điều sau đây là đúng:

  • Thuộc tính currentWordCount được đặt thành 1.
  • Thuộc tính score được đặt thành 0.
  • Thuộc tính isGuessedWordWrong được đặt thành false.
  • Thuộc tính isGameOver được đặt thành false.

Hãy hoàn tất các bước sau để thêm kiểm thử:

  1. Tạo một phương thức gameViewModel_Initialization_FirstWordLoaded() và chú thích phương thức này bằng chú thích @Test:
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {

}
  1. Truy cập vào thuộc tính viewModel.uiState.value để lấy thực thể ban đầu của lớp GameUiState. Chỉ định thực thể đó cho biến currentGameUiState mới chỉ có thể đọc.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val currentGameUiState = viewModel.uiState.value
}
  1. Để lấy từ mà người chơi đoán đúng, hãy dùng hàm getUnscrambleWord(). Hàm này nhận từ currentGameUiState.currentScrambledWord và trả về từ không bị xáo trộn. Chỉ định giá trị trả về cho một biến mới chỉ có thể đọc có tên là unScrambledWord.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val currentGameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

}
  1. Để xác minh trạng thái là chính xác, hãy thêm các hàm assertTrue() để xác nhận là thuộc tính currentWordCount được đặt thành 1, và thuộc tính score được đặt thành 0.
  2. Thêm các hàm assertFalse() để xác minh thuộc tính isGuessedWordWrongfalse, và thuộc tính isGameOver được đặt thành false.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

    // Assert that current word is scrambled.
    assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
    // Assert that current word count is set to 1.
    assertTrue(gameUiState.currentWordCount == 1)
    // Assert that initially the score is 0.
    assertTrue(gameUiState.score == 0)
    // Assert that the wrong word guessed is false.
    assertFalse(gameUiState.isGuessedWordWrong)
    // Assert that game is not over.
    assertFalse(gameUiState.isGameOver)
}
  1. Chạy kiểm thử để xác nhận là kiểm thử đạt.

Một trường hợp kiểm thử giá trị biên (ranh giới) khác là kiểm thử trạng thái giao diện người dùng sau khi người dùng đoán tất cả các từ. Bạn cần xác nhận là khi người dùng đoán chính xác tất cả các từ, thì những điều sau đây là đúng:

  • Điểm số đã được cập nhật,
  • Thuộc tính currentGameUiState.currentWordCount bằng giá trị của hằng số MAX_NO_OF_WORDS, và
  • Thuộc tính currentGameUiState.isGameOver được đặt thành true.

Hãy hoàn tất các bước sau để thêm kiểm thử:

  1. Tạo một phương thức gameViewModel_Initialization_FirstWordLoaded() và chú thích phương thức này bằng chú thích @Test: Trong phương thức này, hãy tạo một biến expectedScore và chỉ định giá trị 0 cho biến đó.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
}
  1. Để lấy trạng thái ban đầu, hãy thêm biến currentGameUiState và chỉ định giá trị của thuộc tính viewModel.uiState.value cho biến đó.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
}
  1. Để lấy từ mà người chơi đoán đúng, hãy dùng hàm getUnscrambleWord(). Hàm này nhận từ currentGameUiState.currentScrambledWord và trả về từ không bị xáo trộn. Lưu trữ giá trị được trả về này trong một biến mới chỉ có thể đọc có tên là unScrambledWord, và chỉ định giá trị mà hàm getUnscrambledWord() trả về.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
  1. Để kiểm thử xem người dùng có đoán ra tất cả các câu trả lời không, hãy sử dụng khối repeat để lặp lại quá trình thực thi phương thức viewModel.updateUserGuess() và phương thức viewModel.checkUserGuess() MAX_NO_OF_WORDS lần.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    repeat(MAX_NO_OF_WORDS) {

    }
}
  1. Trong khối repeat, hãy thêm giá trị của hằng số SCORE_INCREASE vào biến expectedScore để xác nhận việc điểm số sẽ tăng sau mỗi câu trả lời đúng.
  2. Thêm lệnh gọi vào phương thức viewModel.updateUserGuess() và truyền biến correctPlayerWord làm đối số.
  3. Thêm lệnh gọi vào phương thức viewModel.checkUserGuess() để kích hoạt quá trình kiểm tra dự đoán của người dùng.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    repeat(MAX_NO_OF_WORDS) {
        expectedScore += SCORE_INCREASE
        viewModel.updateUserGuess(correctPlayerWord)
        viewModel.checkUserGuess()
    }
}
  1. Để xác minh trạng thái là chính xác, hãy thêm hàm assertEquals() để kiểm tra xem giá trị của thuộc tính currentGameUiState.score có bằng với giá trị của biến expectedScore hay không.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    repeat(MAX_NO_OF_WORDS) {
        expectedScore += SCORE_INCREASE
        viewModel.updateUserGuess(correctPlayerWord)
        viewModel.checkUserGuess()
        currentGameUiState = viewModel.uiState.value
        correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
        // Assert that after each correct answer, score is updated correctly.
        assertEquals(expectedScore, currentGameUiState.score)
    }
}
  1. Thêm một hàm assertEquals() để xác nhận giá trị của thuộc tính currentGameUiState.currentWordCount bằng với giá trị của hằng số MAX_NO_OF_WORDS, và giá trị của thuộc tính currentGameUiState.isGameOver được đặt thành true.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    repeat(MAX_NO_OF_WORDS) {
        expectedScore += SCORE_INCREASE
        viewModel.updateUserGuess(correctPlayerWord)
        viewModel.checkUserGuess()
        currentGameUiState = viewModel.uiState.value
        correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
        // Assert that after each correct answer, score is updated correctly.
        assertEquals(expectedScore, currentGameUiState.score)
    }
    // Assert that after all questions are answered, the current word count is up-to-date.
    assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
    // Assert that after 10 questions are answered, the game is over.
    assertTrue(currentGameUiState.isGameOver)
}
  1. Chạy kiểm thử để xác nhận là kiểm thử đạt.

Tổng quan về vòng đời của thực thể kiểm thử

Khi xem xét kỹ cách khởi chạy viewModel trong kiểm thử, bạn có thể nhận thấy viewModel chỉ chạy một lần mặc dù tất cả các kiểm thử đều sử dụng lớp này. Đoạn mã này cho biết định nghĩa của thuộc tính viewModel.

class GameViewModelTest {
    private val viewModel = GameViewModel()

    @Test
    fun gameViewModel_Initialization_FirstWordLoaded() {
        val gameUiState = viewModel.uiState.value
        ...
    }
    ...
}

Bạn có thể thắc mắc những câu hỏi sau:

  • Điều này có nghĩa là cùng một thực thể của viewModel có được sử dụng lại cho tất cả các kiểm thử không?
  • Việc này có gây ra vấn đề gì không? Ví dụ: điều gì sẽ xảy ra nếu phương thức kiểm thử gameViewModel_Initialization_FirstWordLoaded thực thi sau phương thức kiểm thử gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset? Liệu kiểm thử khởi chạy có thất bại không?

Câu trả lời cho cả hai câu hỏi là không. Các phương thức kiểm thử được thực thi riêng biệt để tránh tác dụng phụ không mong muốn từ trạng thái thực thể kiểm thử có thể thay đổi. Theo mặc định, trước khi thực thi từng phương thức kiểm thử, JUnit sẽ tạo một bản sao mới của lớp kiểm thử.

Vì cho đến nay, bạn đã có 4 phương thức kiểm thử trong lớp GameViewModelTest, nên GameViewModelTest sẽ tạo thực thể 4 lần. Mỗi thực thể đều có bản sao thuộc tính viewModel riêng. Do đó, trình tự thực thi kiểm thử không quan trọng.

5. Giới thiệu về phạm vi sử dụng (mức độ bao phủ) của mã

Mức độ bao phủ của mã đóng vai trò quan trọng trong việc xác định xem bạn có kiểm thử đầy đủ các lớp, phương thức và dòng mã tạo nên ứng dụng của bạn hay không.

Android Studio cung cấp công cụ kiểm thử mức độ bao phủ cho các bài kiểm thử đơn vị cục bộ để theo dõi tỷ lệ và khu vực của mã ứng dụng mà bài kiểm thử đơn vị bao phủ.

Chạy kiểm thử kèm mức độ bao phủ bằng Android Studio

Cách chạy các kiểm thử kèm theo mức độ bao phủ:

  1. Nhấp chuột phải vào tệp GameViewModelTest.kt trong ngăn dự án rồi chọn 28f58fea5649f4d5.png Chạy 'GameViewModelTest' kèm Mức độ bao phủ.

8e76992459e6419e.png

  1. Sau khi hoàn tất quá trình thực thi kiểm thử, trong bảng điều khiển mức độ bao phủ bên phải, hãy nhấp vào lựa chọn Gói Flatten (Làm phẳng).

84f0624dbbb402ee.png

  1. Nhấp đúp vào gói com.example.android.unscramble.ui như trong hình sau.

b064d50424763a7d.png

Bảng điều khiển mức độ bao phủ hiển thị mức độ bao phủ của GameViewModel như trong hình sau.

e0230eece97a81dc.png

Báo cáo phân tích kiểm thử

Báo cáo hiển thị trong biểu đồ sau được chia nhỏ thành hai phần:

  • Tỷ lệ phần trăm các phương thức được bao phủ bởi các bài kiểm thử đơn vị: Trong biểu đồ ví dụ, các kiểm thử bạn đã viết từ trước đến nay bao phủ 7/8 phương thức. Tức là 87% trong tổng số phương thức.
  • Tỷ lệ phần trăm số dòng được bao phủ bởi các kiểm thử đơn vị: Trong biểu đồ ví dụ, các kiểm thử bạn đã viết bao phủ 41/44 dòng mã. Tức là 93% trong tổng số dòng mã.

e0230eece97a81dc.png

Báo cáo cho thấy các bài kiểm thử đơn vị mà bạn đã viết từ trước đến nay đã bỏ sót một số phần của mã. Để xác định phần nào đã bị bỏ lỡ, hãy hoàn tất bước sau:

  • Nhấp đúp vào GameViewModel.

b4e3169a805497e9.png

Android Studio hiện tệp GameViewModel.kt với mã có màu được thêm ở bên trái cửa sổ. Màu xanh lục sáng cho biết các dòng mã đó đã được bao phủ.

9348d72ff2737009.png

Khi cuộn xuống trong GameViewModel, bạn có thể nhận thấy một vài dòng được đánh dấu bằng màu hồng nhạt. Màu này cho biết các dòng mã này không thuộc phạm vi (mức độ bao phủ) của kiểm thử đơn vị.

dd2419cd8af3a486.png

Cải thiện mức độ bao phủ

Để cải thiện mức độ bao phủ, bạn cần viết một kiểm thử bao phủ đường dẫn bị thiếu. Bạn cần thêm một kiểm thử để xác nhận là khi người dùng bỏ qua một từ, thì những điều sau đây là đúng:

  • Thuộc tính currentGameUiState.score vẫn không thay đổi.
  • Thuộc tính currentGameUiState.currentWordCount được tăng thêm một, như thể hiện trong đoạn mã sau đây.

Để chuẩn bị cho việc cải thiện mức độ bao phủ, hãy thêm phương thức kiểm thử sau vào lớp GameViewModelTest.

@Test
fun gameViewModel_WordSkipped_ScoreUnchangedAndWordCountIncreased() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    viewModel.updateUserGuess(correctPlayerWord)
    viewModel.checkUserGuess()

    currentGameUiState = viewModel.uiState.value
    val lastWordCount = currentGameUiState.currentWordCount
    viewModel.skipWord()
    currentGameUiState = viewModel.uiState.value
    // Assert that score remains unchanged after word is skipped.
    assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
    // Assert that word count is increased by 1 after word is skipped.
    assertEquals(lastWordCount + 1, currentGameUiState.currentWordCount)
}

Hãy hoàn tất các bước sau để chạy lại mức độ bao phủ:

  1. Nhấp chuột phải vào tệp GameViewModelTest.kt trong trình đơn rồi chọn 28f58fea5649f4d5.png Chạy 'GameViewModelTest' kèm Mức độ bao phủ.
  2. Để biên dịch lại kết quả sau khi chạy lại, hãy nhấp vào nút Biên dịch lại khi bạn thấy lời nhắc như trong hình sau.

4b938d2efe289fbc.png

  1. Sau khi tạo bản dựng thành công, hãy chuyển đến phần tử GameViewModel lần nữa và xác nhận tỷ lệ phần trăm của mức độ bao phủ là 100%. Báo cáo mức độ bao phủ sau cùng được hiển thị trong hình sau.

c24fe215d806b3fe.png

  1. Di chuyển đến tệp GameViewModel.kt rồi cuộn xuống để kiểm tra xem đường dẫn bị bỏ lỡ trước đó đã được bao phủ hay chưa.

5b96c0b7300e6f06.png

Bạn đã tìm hiểu cách chạy, phân tích và cải thiện mức độ bao phủ mã của mã ứng dụng.

Tỷ lệ phần trăm mức độ bao phủ của mã cao có đồng nghĩa với mã ứng dụng có chất lượng cao không? Không. Mức độ bao phủ của mã cho biết tỷ lệ phần trăm mã được sử dụng hoặc thực thi theo kiểm thử đơn vị. Cột này không cho biết là mã đã được xác minh. Nếu bạn xoá mọi câu nhận định khỏi mã kiểm thử đơn vị và chạy mức độ bao phủ của mã, thì nó vẫn cho thấy mức độ bao phủ là 100%.

Mức độ bao phủ cao không cho biết các bài kiểm thử được thiết kế chính xác và các bài kiểm thử xác minh hành vi của ứng dụng. Cần đảm bảo các kiểm thử bạn đã viết có câu nhận định xác minh hành vi của lớp đang được kiểm thử. Bạn cũng không cần phải cố viết các kiểm thử đơn vị để đạt được mức độ bao phủ kiểm thử 100% cho toàn bộ ứng dụng. Thay vào đó, bạn nên kiểm thử một số phần trong mã của ứng dụng, chẳng hạn như Hoạt động, bằng cách sử dụng các kiểm thử giao diện người dùng.

Tuy nhiên, mức độ bao phủ thấp đồng nghĩa với việc phần lớn mã của bạn chưa được kiểm thử hoàn toàn. Dùng mức độ bao phủ của mã như một công cụ để tìm các phần mã chưa được thực thi trong kiểm thử thay vì như một công cụ để đo lường chất lượng mã.

6. Lấy mã giải pháp

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

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git

Ngoài ra, 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ở trong Android Studio.

Nếu bạn muốn xem mã giải pháp, hãy xem mã đó trên GitHub.

7. Kết luận

Xin chúc mừng! Bạn đã tìm hiểu cách xác định chiến lược kiểm thử và triển khai các kiểm thử đơn vị để kiểm thử ViewModelStateFlow trong ứng dụng Unscramble. Trong quá trình phát triển các ứng dụng Android, hãy nhớ viết kiểm thử cùng với các tính năng của ứng dụng để chắc chắn là ứng dụng của bạn hoạt động đúng cách trong quá trình phát triển.

Tóm tắt

  • Sử dụng cấu hình testImplementation để cho biết các phần phụ thuộc áp dụng cho mã nguồn kiểm thử cục bộ chứ không phải mã ứng dụng.
  • Cố gắng phân loại các kiểm thử thành ba trường hợp: Đường dẫn thành công, đường dẫn lỗi và trường hợp kiểm thử giá trị biên (ranh giới)
  • Một bài kiểm thử đơn vị hiệu quả sẽ có ít nhất bốn đặc điểm: có trọng tâm, dễ hiểu, có tính xác định và độc lập.
  • Các phương thức kiểm thử được thực thi riêng biệt để tránh tác dụng phụ không mong muốn từ trạng thái thực thể kiểm thử có thể thay đổi.
  • Theo mặc định, trước khi mỗi phương thức kiểm thử thực thi, JUnit sẽ tạo một bản sao mới của lớp kiểm thử.
  • Mức độ bao phủ của mã đóng vai trò quan trọng trong việc xác định xem bạn đã kiểm thử đầy đủ các lớp, phương thức và dòng mã tạo nên ứng dụng của bạn hay chưa.

Tìm hiểu thêm