Jetpack Compose has revolutionized Android UI development with its declarative approach. But a beautiful UI is only half the battle; robust testing is essential to ensure that your application functions correctly and remains maintainable. Jetpack Compose provides a comprehensive set of testing APIs designed specifically for Compose UI elements, making it easier to write reliable and effective UI tests.
What are Compose Testing APIs?
Compose Testing APIs are a set of functions and tools provided by Jetpack Compose to facilitate UI testing. These APIs allow developers to interact with composables programmatically, assert their properties, and simulate user actions.
Why Use Compose Testing APIs?
- Precise Targeting: Directly interact with and assert properties of specific composables.
- Lifecycle Management: Integrated seamlessly with Compose’s lifecycle, ensuring tests are reliable.
- Simplified Testing: Reduce boilerplate code and simplify complex UI interactions.
- Increased Reliability: Provide more accurate and less flaky UI tests compared to traditional Android UI testing.
How to Use Compose Testing APIs
Before diving into examples, ensure you have the necessary dependencies in your build.gradle file:
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.1")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.1")
}
Explanation:
ui-test-junit4: Provides the core testing APIs.ui-tooling: Allows you to inspect your composables using the Inspector.ui-test-manifest: Required for running tests on devices without a debuggable application.
Step 1: Setting Up Your Test Environment
Create a test class in your androidTest directory. Use @get:Rule with ComposeTestRule to manage the Compose test environment:
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun myFirstComposeTest() {
// Test logic goes here
}
}
Step 2: Basic UI Test Example
Let’s start with a simple example. Assume you have a composable like this:
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Greeting("Android")
}
Now, create a test to verify the text in the Greeting composable:
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import org.junit.Rule
import org.junit.Test
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testGreetingText() {
composeTestRule.setContent {
Greeting("World")
}
composeTestRule.onNodeWithText("Hello World!").assertExists()
}
}
Explanation:
composeTestRule.setContent: Sets the content of the composable for testing.onNodeWithText("Hello World!"): Finds a node in the UI tree that displays the text “Hello World!”.assertExists(): Asserts that the node exists.
Step 3: Finding Composables
Jetpack Compose testing APIs offer several ways to find composables:
onNodeWithText(String): Finds a composable with specific text.onNodeWithContentDescription(String): Finds a composable with specific content description.onNodeWithTag(String): Finds a composable with a specific test tag.onNode(Matcher): Finds a composable using a custom matcher.
Example using content description:
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@Composable
fun SettingsButton() {
IconButton(
onClick = { /* Handle settings click */ },
content = {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = "Settings",
)
},
modifier = Modifier.testTag("settingsButton")
)
}
And the test:
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import org.junit.Rule
import org.junit.Test
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testSettingsButton() {
composeTestRule.setContent {
SettingsButton()
}
composeTestRule.onNodeWithContentDescription("Settings").assertExists()
}
}
Step 4: Performing Actions
Besides finding composables, you often need to simulate user interactions:
performClick(): Simulates a click action.performTextInput(String): Enters text into a text field.performScrollTo(): Scrolls to a specific position in a scrollable container.
Example with performClick():
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
@Composable
fun MyButton() {
val counter = remember { mutableStateOf(0) }
Button(onClick = { counter.value++ }) {
Text("Clicked ${counter.value} times")
}
}
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testButtonClick() {
composeTestRule.setContent {
MyButton()
}
composeTestRule.onNodeWithText("Clicked 0 times").performClick()
composeTestRule.onNodeWithText("Clicked 1 times").assertExists()
}
}
Step 5: Using Semantics Properties for Testing
Semantics properties provide additional information about UI elements for accessibility and testing. You can set these properties using Modifier.semantics.
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@Composable
fun MyCustomButton(onClick: () -> Unit, text: String) {
Button(
onClick = onClick,
modifier = Modifier.semantics {
contentDescription = "Custom Button"
}
) {
Text(text = text)
}
}
And test it:
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testCustomButtonClick() {
var clicked = false
composeTestRule.setContent {
MyCustomButton(onClick = { clicked = true }, text = "Click Me")
}
composeTestRule.onNodeWithContentDescription("Custom Button").performClick()
assert(clicked)
}
}
Advanced Testing Techniques
1. Using ComposeContentTestRule
For more advanced testing scenarios, use ComposeContentTestRule, which extends ComposeTestRule, providing additional features such as synchronization with Compose’s internal state.
2. Capturing Screenshot
While not a core part of the testing APIs, capturing screenshots during tests can be helpful for visual validation. This requires additional setup with the UiDevice class.
import android.os.Environment
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test
import java.io.File
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testCaptureScreenshot() {
composeTestRule.setContent {
Greeting("Screenshot Test")
}
val filePath = File(Environment.getExternalStorageDirectory(), "screenshot.png")
composeTestRule.onRoot().captureToImage().asAndroidBitmap().compress(Bitmap.CompressFormat.PNG, 100, filePath.outputStream())
}
}
Conclusion
The Compose Testing APIs provide a powerful and intuitive way to test your Jetpack Compose UI elements. By using these APIs, you can write precise, reliable, and maintainable UI tests. Embrace these tools to ensure the quality and robustness of your Compose-based Android applications.