Testing trong Jetpack Compose
Testing là phần quan trọng để đảm bảo chất lượng ứng dụng. Compose cung cấp APIs testing mạnh mẽ để kiểm tra UI.
1. Setup
Dependencies
// build.gradle.kts
dependencies {
// Testing
testImplementation("junit:junit:4.13.2")
// Compose testing
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}2. ComposeTestRule
Basic setup
class MyScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testGreeting() {
composeTestRule.setContent {
Greeting("World")
}
composeTestRule.onNodeWithText("Hello, World!").assertIsDisplayed()
}
}Với Activity
class MainActivityTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun testMainScreen() {
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}3. Finding Elements
By Text
// Exact match
composeTestRule.onNodeWithText("Submit")
// Substring
composeTestRule.onNodeWithText("Submit", substring = true)
// Case insensitive
composeTestRule.onNodeWithText("submit", ignoreCase = true)By Content Description
composeTestRule.onNodeWithContentDescription("Close button")By Test Tag
// In composable
Text(
"Hello",
modifier = Modifier.testTag("greeting")
)
// In test
composeTestRule.onNodeWithTag("greeting")By Semantics
composeTestRule.onNode(
hasText("Button") and hasClickAction()
)
composeTestRule.onNode(
hasContentDescription("Menu") and isEnabled()
)Multiple nodes
// Tất cả nodes với text "Item"
composeTestRule.onAllNodesWithText("Item")
// First node
composeTestRule.onAllNodesWithText("Item")[0]
// Đếm nodes
composeTestRule.onAllNodesWithText("Item").assertCountEquals(5)4. Assertions
Visibility
composeTestRule.onNodeWithText("Title")
.assertIsDisplayed()
composeTestRule.onNodeWithText("Hidden")
.assertDoesNotExist()
composeTestRule.onNodeWithText("Offscreen")
.assertExists() // Exists nhưng có thể không visibleContent
composeTestRule.onNodeWithTag("input")
.assertTextEquals("Hello")
composeTestRule.onNodeWithTag("input")
.assertTextContains("Hel")State
composeTestRule.onNodeWithTag("checkbox")
.assertIsOn()
// hoặc .assertIsOff()
composeTestRule.onNodeWithTag("button")
.assertIsEnabled()
// hoặc .assertIsNotEnabled()
composeTestRule.onNodeWithTag("item")
.assertIsSelected()
// hoặc .assertIsNotSelected()Focus
composeTestRule.onNodeWithTag("input")
.assertIsFocused()
// hoặc .assertIsNotFocused()5. Actions
Click
composeTestRule.onNodeWithText("Submit")
.performClick()Text input
composeTestRule.onNodeWithTag("email")
.performTextInput("test@example.com")
composeTestRule.onNodeWithTag("email")
.performTextClearance()
composeTestRule.onNodeWithTag("email")
.performTextReplacement("new@example.com")Scroll
composeTestRule.onNodeWithTag("list")
.performScrollToIndex(10)
composeTestRule.onNodeWithTag("list")
.performScrollToNode(hasText("Item 10"))Gestures
composeTestRule.onNodeWithTag("swipeable")
.performTouchInput {
swipeLeft()
}
composeTestRule.onNodeWithTag("draggable")
.performTouchInput {
swipeDown(startY = 0f, endY = 500f)
}6. Synchronization
waitUntil
@Test
fun testAsyncContent() {
composeTestRule.setContent {
AsyncScreen()
}
// Đợi loading xong
composeTestRule.waitUntil(timeoutMillis = 5000) {
composeTestRule
.onAllNodesWithText("Loading")
.fetchSemanticsNodes()
.isEmpty()
}
composeTestRule.onNodeWithText("Data loaded").assertIsDisplayed()
}advanceTimeBy (for animations)
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testAnimation() {
composeTestRule.mainClock.autoAdvance = false
composeTestRule.setContent {
AnimatedContent()
}
// Advance time manually
composeTestRule.mainClock.advanceTimeBy(500)
composeTestRule.onNodeWithText("Animated").assertIsDisplayed()
}7. Test Navigation
@Test
fun testNavigation() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
composeTestRule.setContent {
navController.navigatorProvider.addNavigator(ComposeNavigator())
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("detail") { DetailScreen() }
}
}
// Verify initial destination
assertEquals("home", navController.currentBackStackEntry?.destination?.route)
// Navigate
composeTestRule.onNodeWithText("Go to Detail").performClick()
// Verify navigation
assertEquals("detail", navController.currentBackStackEntry?.destination?.route)
}8. Test với ViewModel
@Test
fun testWithViewModel() {
val viewModel = MyViewModel()
composeTestRule.setContent {
MyScreen(viewModel = viewModel)
}
// Verify initial state
composeTestRule.onNodeWithText("Count: 0").assertIsDisplayed()
// Trigger action
composeTestRule.onNodeWithText("Increment").performClick()
// Verify updated state
composeTestRule.onNodeWithText("Count: 1").assertIsDisplayed()
}Với Fake ViewModel
class FakeViewModel : MyViewModel() {
override val items = MutableStateFlow(listOf(Item(1, "Test")))
}
@Test
fun testWithFakeData() {
composeTestRule.setContent {
MyScreen(viewModel = FakeViewModel())
}
composeTestRule.onNodeWithText("Test").assertIsDisplayed()
}9. Screenshot Testing
@Test
fun screenshotTest() {
composeTestRule.setContent {
MyScreen()
}
composeTestRule.onRoot()
.captureToImage()
.asAndroidBitmap()
// So sánh với baseline image
}10. Best Practices
Sử dụng testTag cho elements cần test
// In composable
LazyColumn(
modifier = Modifier.testTag("product_list")
) {
items(products) { product ->
ProductItem(
product = product,
modifier = Modifier.testTag("product_${product.id}")
)
}
}
// In test
composeTestRule.onNodeWithTag("product_list")
.performScrollToNode(hasTestTag("product_5"))
composeTestRule.onNodeWithTag("product_5")
.performClick()Test reusable composables
@Test
fun testButton_enabled() {
composeTestRule.setContent {
MyButton(text = "Click", enabled = true, onClick = { })
}
composeTestRule.onNodeWithText("Click")
.assertIsEnabled()
}
@Test
fun testButton_disabled() {
composeTestRule.setContent {
MyButton(text = "Click", enabled = false, onClick = { })
}
composeTestRule.onNodeWithText("Click")
.assertIsNotEnabled()
}📝 Tóm tắt
| API | Mục đích |
|---|---|
createComposeRule() | Tạo test rule |
onNodeWithText() | Tìm node theo text |
onNodeWithTag() | Tìm node theo test tag |
assertIsDisplayed() | Verify visibility |
performClick() | Click action |
performTextInput() | Type text |
waitUntil() | Đợi condition |
Test Structure
@Test
fun descriptiveTestName() {
// Arrange
composeTestRule.setContent { MyScreen() }
// Act
composeTestRule.onNodeWithText("Button").performClick()
// Assert
composeTestRule.onNodeWithText("Result").assertIsDisplayed()
}Last updated on