Skip to Content

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 visible

Content

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

APIMụ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