Viết bài kiểm thử đơn vị cho ViewModel

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 ảnh sau đây cho thấy bản xem trước của ứng dụng:

bb1e97c357603a27.png

Trong lớp học lập trình Viết bài 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 bài kiểm thử tự động. Bạn cũng đã tìm hiểu cách triển khai các bài 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 bài 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.
  • Bài 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 bài 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, bài 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 bài 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 bài kiểm thử đơn vị trong tệp build.gradle.kts của mô-đun ứng dụng
  • Cách tạo chiến lược kiểm thử để triển khai các bài kiểm thử đơn vị
  • Cách viết các bài kiểm thử đơn vị bằng JUnit4 và hiểu vòng đời của thực thể kiểm thử
  • Cách chạy, phân tích và cải thiện mức độ sử dụng mã

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

  • Các bài 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 đầu

Để bắt đầu, hãy tải mã khởi đầu 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 đầu

Ở 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 test (kiểm thử) thuộc thư mục src như trong hình ảnh sau:

Thư mục test (kiểm thử) trong ngăn dự án Android Studio

Mã khởi đầu có tệp sau đây:

  • 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 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 bài kiểm thử đơn vị. Để sử dụng khung này, bạn cần thêm khung làm một phần phụ thuộc trong tệp build.gradle.kts 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.6.1")
}

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 không nên đưa mã kiểm thử đơn vị vào 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, 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ã xử lý ứ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.kts. 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.kts của mô-đun app, nằm ở thư mục app trong ngăn Project (Dự án).

Tệp build.gradle.kts trong ngăn dự án

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

android {
    ...
}

dependencies {
    ...
    testImplementation("junit:junit:4.13.2")
}
  1. Trên thanh thông báo ở đầu tệp build.gradle.kts, 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 đây:

1c20fc10750ca60c.png

Bảng kê khai thành phần (BOM) của Compose

Bạn nên dùng BOM của Compose để quản lý các phiên bản thư viện Compose. BOM giúp bạn quản lý mọi phiên bản thư viện Compose mà chỉ cần chỉ định phiên bản của BOM.

Hãy lưu ý phần phụ thuộc trong tệp build.gradle.kts của mô-đun app.

// No need to copy over
// This is part of starter code
dependencies {

   // Import the Compose BOM
    implementation (platform("androidx.compose:compose-bom:2023.06.01"))
    ...
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    ...
}

Chú ý những điều sau:

  • Số phiên bản của thư viện Compose chưa được chỉ định.
  • BOM được nhập bằng implementation platform("androidx.compose:compose-bom:2023.06.01")

Nguyên nhân là do BOM vốn đã có các đường liên kết đến những phiên bản ổn định mới nhất của các thư viện Compose khác nhau, theo cách tương thích với nhau. Khi sử dụng BOM trong ứng dụng, bạn không cần thêm phiên bản nào vào chính các phần phụ thuộc của thư viện Compose. Khi bạn cập nhật phiên bản BOM, tất cả các thư viện mà bạn đang sử dụng sẽ tự động được cập nhật lên phiên bản mới.

Để sử dụng BOM với các thư viện kiểm thử Compose (kiểm thử đo lường), bạn cần nhập androidTestImplementation platform("androidx.compose:compose-bom:xxxx.xx.xx"). Bạn có thể tạo một biến và sử dụng lại biến đó cho implementationandroidTestImplementation như ví dụ minh hoạ.

// Example, not need to copy over
dependencies {

   // Import the Compose BOM
    implementation(platform("androidx.compose:compose-bom:2023.06.01"))
    implementation("androidx.compose.material:material")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")

    // ...
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")

}

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 và nắm được kiến thức về BOM. Giờ thì bạn đã sẵn sàng để thêm một số bài 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à giới hạn của mã. Ở cấp độ cơ bản nhất, bạn có thể phân loại các bài kiểm thử theo 3 trường hợp: đường dẫn thành công, đường dẫn lỗi và trường hợp kiểm thử 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ử hành trình suôn sẻ) tập trung vào việc kiểm thử chức năng của một luồng dương. Luồng dương là luồng không có điều kiện ngoại lệ hoặc lỗi. So với các trường hợp kiểm thử đường dẫn lỗi và kiểm thử 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ì chúng 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 không hợp lệ của người dùng. 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 số và số từ sẽ không được cập nhật.

  • Trường hợp kiểm thử 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 ranh giới trong ứng dụng. Trong ứng dụng Unscramble, kiểm thử ranh giới là 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 bài 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 bài kiểm thử:

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

57d004ccc4d75833.png

f98067499852bdce.png

Để viết bài 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 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 bài 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()  {
    }
}
  1. Nhập các mục sau đây:
import org.junit.Test

Để 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à gán 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 getUnscrambledWord(), 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 unScrambledWord = 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ữ thực thể đó 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à xem điểm số đã được cập nhật hay chưa, hãy dùng hàm assertFalse() để xác minh thuộc tính currentGameUiState.isGuessedWordWrongfalse và dùng hàm assertEquals() để xác minh rằng 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. Nhập các mục sau đây:
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
  1. Để có thể đọc và dùng lại giá trị 20, hãy tạo một đối tượng đồng hành và gán 20 cho 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 xác đị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 được giữ nguyên.
  • Giá trị của thuộc tính currentGameUiState.isGuessedWordWrong được đặt 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 bài 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à gán giá trị "and" cho biến đó. Giá trị này không được 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à gán giá trị của trạng thái viewModel.uiState.value cho biến đó.
  2. Dùng các hàm nhận định để xác nhận rằng 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. Nhập các mục sau đây:
import org.junit.Assert.assertTrue
  1. Chạy kiểm thử để xác nhận là bài kiểm thử đạt.

Trường hợp kiểm thử 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 bài kiểm thử đơn vị cho lớp GameViewModel. Bài kiểm thử này phải xác nhận rằng khi khởi động 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 bài 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ú giải @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 gameUiState mới chỉ có thể đọc.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
}
  1. Để lấy từ mà người chơi đoán đúng, hãy dùng hàm getUnscrambledWord(). Hàm này nhận từ gameUiState.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 gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.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 rằng 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. Nhập các mục sau đây:
import org.junit.Assert.assertNotEquals
  1. Chạy kiểm thử để xác nhận là bài kiểm thử đạt.

Một trường hợp kiểm thử 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;
  • Thuộc tính currentGameUiState.isGameOver được đặt thành true.

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

  1. Tạo một phương thức gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() và chú thích phương thức này bằng chú giải @Test: Trong phương thức này, hãy tạo một biến expectedScore và gán 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à gán 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 getUnscrambledWord(). 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à correctPlayerWord 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 tra xem người dùng có đoán ra tất cả đáp án hay 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 rằng đ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 một 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. Cập nhật từ hiện tại của người chơi, dùng hàm getUnscrambledWord(). Hàm này nhận currentGameUiState.currentScrambledWord làm đối số 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à correctPlayerWord.. Để 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 rằng giá trị của thuộc tính currentGameUiState.currentWordCount bằng 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. Nhập các mục sau đây:
import com.example.unscramble.data.MAX_NO_OF_WORDS
  1. Chạy kiểm thử để xác nhận là bài 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 viewModel khởi động trong quá trình kiểm thử, bạn có thể nhận thấy viewModel chỉ khởi động một lần mặc dù tất cả các trường hợp kiểm thử đều sử dụng nó. Đ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 bài 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 bài kiểm thử khi khởi động 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 thực thể 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ề mức độ sử dụng mã

Mức độ sử dụng 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ã cấu thành 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 bài kiểm thử kèm theo mức độ sử dụng:

  1. Nhấp chuột phải vào tệp GameViewModelTest.kt trong ngăn dự án rồi chọn 28f58fea5649f4d5.png Run 'GameViewModelTest' with Coverage (Chạy 'GameViewModelTest' kèm theo mức độ sử dụng).

ngăn dự án, trong đó tuỳ chọn Run 'GameViewModelTest' with Coverage (Chạy 'GameViewModelTest' kèm theo mức độ sử dụng) được chọn

  1. Sau khi hoàn tất bài kiểm thử, trong ngăn Coverage (mức độ sử dụng) ở bên phải, hãy nhấp vào tuỳ chọn Flatten Packages (Làm phẳng gói).

Tuỳ chọn Flatten Packages (Làm phẳng gói) được làm nổi bật

  1. Hãy lưu ý gói com.example.android.unscramble.ui như trong hình ảnh sau đây.

a4408d8870366144.png

  1. Nhấp đúp vào tên gói com.example.android.unscramble.ui, biểu thị mức độ sử dụng GameViewModel như trong hình ảnh sau đây:

3ec7ea7896b52f3a.png

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

Báo cáo hiển thị trong biểu đồ sau được tách thành 2 phần:

  • Tỷ lệ phần trăm các phương thức có trong các bài kiểm thử đơn vị: Trong biểu đồ ví dụ, các bài kiểm thử bạn đã viết từ trước đến nay bao gồm 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ó trong các bài kiểm thử đơn vị: Trong biểu đồ ví dụ, các bài kiểm thử bạn đã viết bao gồm 39/41 dòng mã. Tức là 95% trong tổng số dòng mã.

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.

d78155448e2b9304.png

Android Studio hiện tệp GameViewModel.kt với thao tác lập trình màu sắc bổ sung ở 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 di chuyển xuống trong GameViewModel, bạn có thể nhận thấy một số 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 độ sử dụng

Để 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 bài kiểm thử để xác nhận rằng 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 độ sử dụng, 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 Run ‘GameViewModelTest' with Coverage (Chạy 'GameViewModelTest' kèm Mức độ sử dụng).
  2. Để biên dịch lại kết quả sau khi chạy lại, hãy nhấp vào nút Recompile (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 độ sử dụng là 100%. Hình ảnh sau đây cho thấy báo cáo mức độ sử dụng sau cùng.

e91469b284854b8c.png

  1. Chuyển đến tệp GameViewModel.kt rồi cuộn xuống để kiểm tra xem đường dẫn bị thiếu trước đó hiện đã được sử dụng 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 độ sử dụng mã của mã xử lý ứng dụng.

Tỷ lệ phần trăm mức độ sử dụng mã cao có đồng nghĩa với mã ứng dụng có chất lượng cao không? Không. Mức độ sử dụng mã cho biết tỷ lệ phần trăm mã được dùng hoặc thực thi theo bài 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 độ sử dụng mã, thì nó vẫn cho thấy mức độ sử dụng 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 bài 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 bài kiểm thử đơn vị để đạt được mức độ sử dụng bài 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 bài kiểm thử giao diện người dùng.

Tuy nhiên, mức độ sử dụng 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 độ sử dụng mã như một công cụ để tìm các phần mã chưa được thực thi trong quá trình 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 mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh git sau:

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

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi 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ử cũng như triển khai các bài kiểm thử đơn vị để kiểm thử ViewModelStateFlow trong ứng dụng Unscramble. Trong tiến trình tạo dựng các ứng dụng Android, hãy nhớ viết bài kiểm thử cùng với các tính năng của ứng dụng để xác nhận rằng ứng dụng của bạn hoạt động đúng cách xuyên suốt 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ã xử lý ứng dụng.
  • Cố gắng phân loại các bài 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ử ranh giới
  • Một bài kiểm thử đơn vị hiệu quả sẽ có ít nhất 4 đặ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ã cấu thành nên ứng dụng của bạn hay chưa.

Tìm hiểu thêm