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:
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ự động và ViewModel 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 đây:
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úpgetUnscrambledWord()
để 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:
- Mở tệp
build.gradle.kts
của mô-đunapp
, nằm ở thư mụcapp
trong ngăn Project (Dự án).
- 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ìnhtestImplementation
chojunit
.
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- 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:
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 implementation
và androidTestImplementation
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
score
vàisGuessedWordWrong
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ử:
- 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:
Để 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.
- Trong phần nội dung của lớp
GameViewModelTest
, hãy khai báo thuộc tínhviewModel
và gán một thực thể của lớpGameViewModel
cho thuộc tính đó.
class GameViewModelTest {
private val viewModel = GameViewModel()
}
- Để 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() {
}
}
- 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.
- Trong phần nội dung hàm, hãy tạo một biến
currentGameUiState
và gán biến đó choviewModel.uiState.value
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
}
- Để lấy người chơi đoán đúng, hãy sử dụng hàm
getUnscrambledWord()
, hàm này nhậncurrentGameUiState.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àcorrectPlayerWord
và chỉ định giá trị mà hàmgetUnscrambledWord()
trả về.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Để 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ếncorrectPlayerWord
vào làm đối số. Sau đó, hãy thêm một lệnh gọi vào phương thứcviewModel.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.
- Lấy thực thể của lớp
GameUiState
từ giá trị của thuộc tínhviewModel.uiState
và lưu trữ thực thể đó trong biếncurrentGameUiState
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
}
- Để 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ínhcurrentGameUiState.isGuessedWordWrong
làfalse
và dùng hàmassertEquals()
để xác minh rằng giá trị của thuộc tínhcurrentGameUiState.score
bằng20
.
@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)
}
- Nhập các mục sau đây:
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
- Để 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án20
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
}
}
- Chạy kiểm thử.
Như trong ảnh chụp màn hình sau đây, bài kiểm thử sẽ có kết quả đạt vì mọi câu nhận định đều hợp lệ:
Đườ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ànhtrue
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ử:
- Trong phần nội dung của lớp
GameViewModelTest
, hãy tạo một hàmgameViewModel_IncorrectGuess_ErrorFlagSet()
và chú thích hàm này bằng chú thích@Test
.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
}
- 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"
}
- Thêm lệnh gọi vào phương thức
viewModel.updateUserGuess()
và truyền biếnincorrectPlayerWord
làm đối số. - 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()
}
- Thêm biến
currentGameUiState
và gán giá trị của trạng tháiviewModel.uiState.value
cho biến đó. - Dùng các hàm nhận định để xác nhận rằng giá trị của thuộc tính
currentGameUiState.score
là0
và giá trị của thuộc tínhcurrentGameUiState.isGuessedWordWrong
được đặt thànhtrue
.
@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)
}
- Nhập các mục sau đây:
import org.junit.Assert.assertTrue
- 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ành1
. - Thuộc tính
score
được đặt thành0
. - Thuộc tính
isGuessedWordWrong
được đặt thànhfalse
. - Thuộc tính
isGameOver
được đặt thànhfalse
.
Hãy hoàn tất các bước sau để thêm bài kiểm thử:
- 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() {
}
- Truy cập vào thuộc tính
viewModel.uiState.value
để lấy thực thể ban đầu của lớpGameUiState
. Chỉ định thực thể đó cho biếngameUiState
mới chỉ có thể đọc.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
}
- Để 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)
}
- Để 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ínhcurrentWordCount
được đặt thành1
và thuộc tínhscore
được đặt thành0
. - Thêm các hàm
assertFalse()
để xác minh thuộc tínhisGuessedWordWrong
làfalse
và thuộc tínhisGameOver
được đặt thànhfalse
.
@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)
}
- Nhập các mục sau đây:
import org.junit.Assert.assertNotEquals
- 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ànhtrue
.
Hãy hoàn tất các bước sau để thêm bài kiểm thử:
- 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ếnexpectedScore
và gán giá trị0
cho biến đó.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
}
- Để 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ínhviewModel.uiState.value
cho biến đó.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
}
- Để 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àmgetUnscrambledWord()
trả về.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Để 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ứcviewModel.updateUserGuess()
và phương thứcviewModel.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) {
}
}
- Trong khối
repeat
, hãy thêm giá trị của hằng sốSCORE_INCREASE
vào biếnexpectedScore
để xác nhận rằng điểm số sẽ tăng sau mỗi câu trả lời đúng. - Thêm lệnh gọi vào phương thức
viewModel.updateUserGuess()
và truyền biếncorrectPlayerWord
làm đối số. - 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()
}
}
- 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ậncurrentGameUiState.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àmassertEquals()
để kiểm tra xem giá trị của thuộc tínhcurrentGameUiState.score
có bằng với giá trị của biếnexpectedScore
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)
}
}
- Thêm một hàm
assertEquals()
để xác nhận rằng giá trị của thuộc tínhcurrentGameUiState.currentWordCount
bằng giá trị của hằng sốMAX_NO_OF_WORDS
và giá trị của thuộc tínhcurrentGameUiState.isGameOver
được đặt thànhtrue
.
@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)
}
- Nhập các mục sau đây:
import com.example.unscramble.data.MAX_NO_OF_WORDS
- 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:
- Nhấp chuột phải vào tệp
GameViewModelTest.kt
trong ngăn dự án rồi chọn Run ‘GameViewModelTest' with Coverage (Chạy "GameViewModelTest" kèm theo mức độ sử dụng).
- 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).
- Bạn sẽ thấy gói
com.example.android.unscramble.ui
như trong hình ảnh sau đây.
- Nhấp đúp vào tên gói
com.example.android.unscramble.ui
, hiển thị mức độ sử dụngGameViewModel
như trong hình ảnh sau đây:
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.
Android Studio hiển thị tệp GameViewModel.kt
kèm theo mã màu 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 sử dụng.
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ã như vậy không được sử dụng trong bài kiểm thử đơn vị.
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 đây để chạy lại mức độ sử dụng:
- Nhấp chuột phải vào tệp
GameViewModelTest.kt
và trong trình đơn, chọn Run ‘GameViewModelTest' with Coverage (Chạy "GameViewModelTest" kèm theo mức độ sử dụng). - 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.
- 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.
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ử ViewModel
và StateFlow
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.