Bỏ qua để đến nội dung

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.

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")
}

class MyScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testGreeting() {
composeTestRule.setContent {
Greeting("World")
}
composeTestRule.onNodeWithText("Hello, World!").assertIsDisplayed()
}
}
class MainActivityTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun testMainScreen() {
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}

// Exact match
composeTestRule.onNodeWithText("Submit")
// Substring
composeTestRule.onNodeWithText("Submit", substring = true)
// Case insensitive
composeTestRule.onNodeWithText("submit", ignoreCase = true)
composeTestRule.onNodeWithContentDescription("Close button")
// In composable
Text(
"Hello",
modifier = Modifier.testTag("greeting")
)
// In test
composeTestRule.onNodeWithTag("greeting")
composeTestRule.onNode(
hasText("Button") and hasClickAction()
)
composeTestRule.onNode(
hasContentDescription("Menu") and isEnabled()
)
// Tất cả nodes với text "Item"
composeTestRule.onAllNodesWithText("Item")
// First node
composeTestRule.onAllNodesWithText("Item")[0]
// Đếm nodes
composeTestRule.onAllNodesWithText("Item").assertCountEquals(5)

composeTestRule.onNodeWithText("Title")
.assertIsDisplayed()
composeTestRule.onNodeWithText("Hidden")
.assertDoesNotExist()
composeTestRule.onNodeWithText("Offscreen")
.assertExists() // Exists nhưng có thể không visible
composeTestRule.onNodeWithTag("input")
.assertTextEquals("Hello")
composeTestRule.onNodeWithTag("input")
.assertTextContains("Hel")
composeTestRule.onNodeWithTag("checkbox")
.assertIsOn()
// hoặc .assertIsOff()
composeTestRule.onNodeWithTag("button")
.assertIsEnabled()
// hoặc .assertIsNotEnabled()
composeTestRule.onNodeWithTag("item")
.assertIsSelected()
// hoặc .assertIsNotSelected()
composeTestRule.onNodeWithTag("input")
.assertIsFocused()
// hoặc .assertIsNotFocused()

composeTestRule.onNodeWithText("Submit")
.performClick()
composeTestRule.onNodeWithTag("email")
.performTextInput("test@example.com")
composeTestRule.onNodeWithTag("email")
.performTextClearance()
composeTestRule.onNodeWithTag("email")
.performTextReplacement("new@example.com")
composeTestRule.onNodeWithTag("list")
.performScrollToIndex(10)
composeTestRule.onNodeWithTag("list")
.performScrollToNode(hasText("Item 10"))
composeTestRule.onNodeWithTag("swipeable")
.performTouchInput {
swipeLeft()
}
composeTestRule.onNodeWithTag("draggable")
.performTouchInput {
swipeDown(startY = 0f, endY = 500f)
}

@Test
fun testAsyncContent() {
composeTestRule.setContent {
AsyncScreen()
}
// Đợi loading xong
composeTestRule.waitUntil(timeoutMillis = 5000) {
composeTestRule
.onAllNodesWithText("Loading")
.fetchSemanticsNodes()
.isEmpty()
}
composeTestRule.onNodeWithText("Data loaded").assertIsDisplayed()
}
@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()
}

@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)
}

@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()
}
class FakeViewModel : MyViewModel() {
override val items = MutableStateFlow(listOf(Item(1, "Test")))
}
@Test
fun testWithFakeData() {
composeTestRule.setContent {
MyScreen(viewModel = FakeViewModel())
}
composeTestRule.onNodeWithText("Test").assertIsDisplayed()
}

@Test
fun screenshotTest() {
composeTestRule.setContent {
MyScreen()
}
composeTestRule.onRoot()
.captureToImage()
.asAndroidBitmap()
// So sánh với baseline image
}

// 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
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()
}

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
fun descriptiveTestName() {
// Arrange
composeTestRule.setContent { MyScreen() }
// Act
composeTestRule.onNodeWithText("Button").performClick()
// Assert
composeTestRule.onNodeWithText("Result").assertIsDisplayed()
}