Testing
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
Phần tiêu đề “1. Setup”Dependencies
Phần tiêu đề “Dependencies”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
Phần tiêu đề “2. ComposeTestRule”Basic setup
Phần tiêu đề “Basic setup”class MyScreenTest { @get:Rule val composeTestRule = createComposeRule()
@Test fun testGreeting() { composeTestRule.setContent { Greeting("World") }
composeTestRule.onNodeWithText("Hello, World!").assertIsDisplayed() }}Với Activity
Phần tiêu đề “Với Activity”class MainActivityTest { @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test fun testMainScreen() { composeTestRule.onNodeWithText("Welcome").assertIsDisplayed() }}3. Finding Elements
Phần tiêu đề “3. Finding Elements”By Text
Phần tiêu đề “By Text”// Exact matchcomposeTestRule.onNodeWithText("Submit")
// SubstringcomposeTestRule.onNodeWithText("Submit", substring = true)
// Case insensitivecomposeTestRule.onNodeWithText("submit", ignoreCase = true)By Content Description
Phần tiêu đề “By Content Description”composeTestRule.onNodeWithContentDescription("Close button")By Test Tag
Phần tiêu đề “By Test Tag”// In composableText( "Hello", modifier = Modifier.testTag("greeting"))
// In testcomposeTestRule.onNodeWithTag("greeting")By Semantics
Phần tiêu đề “By Semantics”composeTestRule.onNode( hasText("Button") and hasClickAction())
composeTestRule.onNode( hasContentDescription("Menu") and isEnabled())Multiple nodes
Phần tiêu đề “Multiple nodes”// Tất cả nodes với text "Item"composeTestRule.onAllNodesWithText("Item")
// First nodecomposeTestRule.onAllNodesWithText("Item")[0]
// Đếm nodescomposeTestRule.onAllNodesWithText("Item").assertCountEquals(5)4. Assertions
Phần tiêu đề “4. Assertions”Visibility
Phần tiêu đề “Visibility”composeTestRule.onNodeWithText("Title") .assertIsDisplayed()
composeTestRule.onNodeWithText("Hidden") .assertDoesNotExist()
composeTestRule.onNodeWithText("Offscreen") .assertExists() // Exists nhưng có thể không visibleContent
Phần tiêu đề “Content”composeTestRule.onNodeWithTag("input") .assertTextEquals("Hello")
composeTestRule.onNodeWithTag("input") .assertTextContains("Hel")State
Phần tiêu đề “State”composeTestRule.onNodeWithTag("checkbox") .assertIsOn() // hoặc .assertIsOff()
composeTestRule.onNodeWithTag("button") .assertIsEnabled() // hoặc .assertIsNotEnabled()
composeTestRule.onNodeWithTag("item") .assertIsSelected() // hoặc .assertIsNotSelected()Focus
Phần tiêu đề “Focus”composeTestRule.onNodeWithTag("input") .assertIsFocused() // hoặc .assertIsNotFocused()5. Actions
Phần tiêu đề “5. Actions”Click
Phần tiêu đề “Click”composeTestRule.onNodeWithText("Submit") .performClick()Text input
Phần tiêu đề “Text input”composeTestRule.onNodeWithTag("email")
composeTestRule.onNodeWithTag("email") .performTextClearance()
composeTestRule.onNodeWithTag("email")Scroll
Phần tiêu đề “Scroll”composeTestRule.onNodeWithTag("list") .performScrollToIndex(10)
composeTestRule.onNodeWithTag("list") .performScrollToNode(hasText("Item 10"))Gestures
Phần tiêu đề “Gestures”composeTestRule.onNodeWithTag("swipeable") .performTouchInput { swipeLeft() }
composeTestRule.onNodeWithTag("draggable") .performTouchInput { swipeDown(startY = 0f, endY = 500f) }6. Synchronization
Phần tiêu đề “6. Synchronization”waitUntil
Phần tiêu đề “waitUntil”@Testfun testAsyncContent() { composeTestRule.setContent { AsyncScreen() }
// Đợi loading xong composeTestRule.waitUntil(timeoutMillis = 5000) { composeTestRule .onAllNodesWithText("Loading") .fetchSemanticsNodes() .isEmpty() }
composeTestRule.onNodeWithText("Data loaded").assertIsDisplayed()}advanceTimeBy (for animations)
Phần tiêu đề “advanceTimeBy (for animations)”@get:Ruleval composeTestRule = createComposeRule()
@Testfun testAnimation() { composeTestRule.mainClock.autoAdvance = false
composeTestRule.setContent { AnimatedContent() }
// Advance time manually composeTestRule.mainClock.advanceTimeBy(500)
composeTestRule.onNodeWithText("Animated").assertIsDisplayed()}7. Test Navigation
Phần tiêu đề “7. Test Navigation”@Testfun 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
Phần tiêu đề “8. Test với ViewModel”@Testfun 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
Phần tiêu đề “Với Fake ViewModel”class FakeViewModel : MyViewModel() { override val items = MutableStateFlow(listOf(Item(1, "Test")))}
@Testfun testWithFakeData() { composeTestRule.setContent { MyScreen(viewModel = FakeViewModel()) }
composeTestRule.onNodeWithText("Test").assertIsDisplayed()}9. Screenshot Testing
Phần tiêu đề “9. Screenshot Testing”@Testfun screenshotTest() { composeTestRule.setContent { MyScreen() }
composeTestRule.onRoot() .captureToImage() .asAndroidBitmap() // So sánh với baseline image}10. Best Practices
Phần tiêu đề “10. Best Practices”Sử dụng testTag cho elements cần test
Phần tiêu đề “Sử dụng testTag cho elements cần test”// In composableLazyColumn( modifier = Modifier.testTag("product_list")) { items(products) { product -> ProductItem( product = product, modifier = Modifier.testTag("product_${product.id}") ) }}
// In testcomposeTestRule.onNodeWithTag("product_list") .performScrollToNode(hasTestTag("product_5"))
composeTestRule.onNodeWithTag("product_5") .performClick()Test reusable composables
Phần tiêu đề “Test reusable composables”@Testfun testButton_enabled() { composeTestRule.setContent { MyButton(text = "Click", enabled = true, onClick = { }) }
composeTestRule.onNodeWithText("Click") .assertIsEnabled()}
@Testfun testButton_disabled() { composeTestRule.setContent { MyButton(text = "Click", enabled = false, onClick = { }) }
composeTestRule.onNodeWithText("Click") .assertIsNotEnabled()}📝 Tóm tắt
Phần tiêu đề “📝 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
Phần tiêu đề “Test Structure”@Testfun descriptiveTestName() { // Arrange composeTestRule.setContent { MyScreen() }
// Act composeTestRule.onNodeWithText("Button").performClick()
// Assert composeTestRule.onNodeWithText("Result").assertIsDisplayed()}