Testing in Jetpack Compose: Unit Tests and UI Tests for Composables

Testing in Jetpack Compose: Unit Tests and UI Tests for Composables

Part of the series "Android Development Series by Mircea Ioan Soit"

Writing tests for your app is crucial to ensuring its reliability, maintainability, and overall quality. Jetpack Compose brings some changes to how we traditionally test Android UIs, offering powerful tools to write both unit tests and UI tests for composables.

In this article, we’ll explore how to test your composables in Jetpack Compose, focusing on both unit testing and UI testing approaches.

1. Why Testing in Compose is Different

Jetpack Compose is a declarative UI framework, which means the UI is built based on state. This declarative nature changes the way we approach testing. Instead of interacting with Activity or Fragment views, we now test composables directly and validate their behavior based on the state they are rendering.

Compose offers the following testing tools:

  • Compose Unit Testing: Tests individual composables by providing a mock environment.
  • Compose UI Testing: Runs end-to-end tests by simulating user interactions.

2. Unit Testing Composables

Unit tests in Compose focus on testing the logic and behavior of individual composables, ensuring they correctly reflect the state passed to them.

a) Setting Up Compose Unit Testing

To begin testing, include the necessary dependencies in your build.gradle:

testImplementation "androidx.compose.ui:ui-test-junit4:<compose-version>"
testImplementation "androidx.compose.ui:ui-tooling:<compose-version>"        

You’ll typically test your composables using the createComposeRule() test rule. Here’s a basic example.

b) Basic Unit Test for a Composable

Let’s create a simple Greeting composable and write a unit test for it:

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

@Test
fun greetingDisplaysCorrectName() {
    val composeTestRule = createComposeRule()

    composeTestRule.setContent {
        Greeting("Compose")
    }

    composeTestRule
        .onNodeWithText("Hello, Compose!")
        .assertExists()
}        

In this test:

  • We use createComposeRule() to create a testing environment for the composable.
  • The composable is rendered inside the setContent block.
  • We validate that the text "Hello, Compose!" exists in the composable by using onNodeWithText() and assertExists().

c) Testing State in Composables

You can also test how your composable behaves when state changes:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

@Test
fun counterIncrementsWhenButtonClicked() {
    val composeTestRule = createComposeRule()

    composeTestRule.setContent {
        Counter()
    }

    // Verify initial state
    composeTestRule
        .onNodeWithText("Count: 0")
        .assertExists()

    // Simulate button click
    composeTestRule
        .onNodeWithText("Increment")
        .performClick()

    // Verify state after click
    composeTestRule
        .onNodeWithText("Count: 1")
        .assertExists()
}        

Here, we verify that the count is initially 0 and then simulate a button click to check if the counter updates to 1. Compose makes it easy to simulate user interactions such as clicks or text input during unit testing.

3. UI Testing in Jetpack Compose

UI testing is essential for validating how users interact with your app. It checks that the app’s UI responds correctly to various actions and ensures the correct UI elements are rendered.

Compose uses Espresso-like APIs to test the UI. It includes a set of tools that let you interact with the composables and perform actions, like clicking buttons or entering text.

a) Basic Setup for UI Tests

Add the following dependencies to your build.gradle for UI testing:

androidTestImplementation "androidx.compose.ui:ui-test-junit4:<compose-version>"
androidTestImplementation "androidx.compose.ui:ui-tooling:<compose-version>"        

You will also need to use createAndroidComposeRule() to create a testing environment that simulates real UI interactions.

b) Writing a UI Test for a Composable

Let’s write a UI test for a simple login form:

@Composable
fun LoginForm(onLoginClick: (String) -> Unit) {
    var username by remember { mutableStateOf("") }

    Column {
        TextField(value = username, onValueChange = { username = it })
        Button(onClick = { onLoginClick(username) }) {
            Text("Login")
        }
    }
}

@Test
fun loginButtonDisplaysCorrectUsername() {
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    composeTestRule.setContent {
        LoginForm {}
    }

    // Enter text in the username field
    composeTestRule
        .onNodeWithText("Username")
        .performTextInput("Mircea")

    // Click the login button
    composeTestRule
        .onNodeWithText("Login")
        .performClick()

    // Verify the username was entered
    composeTestRule
        .onNodeWithText("Mircea")
        .assertExists()
}        

In this UI test:

  • We use performTextInput() to simulate entering text into a TextField.
  • We simulate a button click with performClick().
  • Finally, we verify that the username was entered correctly by checking if it exists in the UI with assertExists().

c) Testing Navigation Between Composables

When testing navigation in Compose, you want to ensure that the correct screen appears after an action, such as clicking a button.

@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = { navController.navigate("details") }) {
        Text("Go to Details")
    }
}

@Composable
fun DetailsScreen() {
    Text("Details Screen")
}

@Test
fun navigateToDetailsScreen() {
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    composeTestRule.setContent {
        val navController = rememberNavController()
        NavHost(navController = navController, startDestination = "home") {
            composable("home") { HomeScreen(navController) }
            composable("details") { DetailsScreen() }
        }
    }

    // Simulate button click to navigate to details
    composeTestRule
        .onNodeWithText("Go to Details")
        .performClick()

    // Verify navigation occurred
    composeTestRule
        .onNodeWithText("Details Screen")
        .assertExists()
}        

This test simulates navigation from a HomeScreen to a DetailsScreen. After clicking the button, the test checks if the "Details Screen" text is displayed, confirming that the navigation was successful.

4. Testing Complex Composables

You may have more complex composables involving animations, gestures, or conditional rendering. Jetpack Compose offers various testing utilities to handle these cases:

  • Animations: Use advanceTimeBy() to test time-based animations.
  • Gestures: Use performTouchInput to simulate touch gestures like swipes or drags.
  • Conditional rendering: Test if specific composables exist based on conditional logic by combining onNode queries with assertions.

Example: Testing AnimatedVisibility

@Composable
fun AnimatedMessage(isVisible: Boolean) {
    AnimatedVisibility(visible = isVisible) {
        Text("Hello Compose!")
    }
}

@Test
fun animatedMessageVisibility() {
    val composeTestRule = createComposeRule()

    composeTestRule.setContent {
        var isVisible by remember { mutableStateOf(false) }
        AnimatedMessage(isVisible = isVisible)
    }

    // Initially, the text should not be visible
    composeTestRule
        .onNodeWithText("Hello Compose!")
        .assertDoesNotExist()

    // Change visibility and verify the text appears
    composeTestRule.runOnIdle { isVisible = true }

    composeTestRule
        .onNodeWithText("Hello Compose!")
        .assertExists()
}        

This example tests if the composable’s content becomes visible when the state changes, simulating an animation toggle.

5. Best Practices for Testing in Compose

  • Write tests early: Start testing from the beginning to ensure the composables behave correctly from the get-go.
  • Mock dependencies: If your composables rely on external services, mock those services to isolate your tests.
  • Use composeTestRule.runOnIdle: This ensures that the test waits for Compose to finish rendering before checking state or simulating user interactions.
  • Test multiple scenarios: Ensure to cover all user flows and edge cases to improve app reliability.

6. Conclusion: Making Testing a Habit

Testing your composables is essential for building robust Android apps in Jetpack Compose. With the tools provided by Compose for unit testing and UI testing, you can ensure your UI components behave as expected under various conditions.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了