Jetpack Compose UI Testing in Android

Jetpack Compose UI Testing in Android

Like always we will directly jump into the implementation part and learn the things on the go…

Dependency:

// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

As you can see, Compose testing is only supported with JUnit4. And ui-test-manifest dependency is required for Android-specific testing tasks.

Implementation:

If the androidTest directory is already not available in your module, then you can also generate it.

Steps:

  1. Add these above dependencies.

  2. Sync the gradle.

  3. Go to run

  4. Click on Record Espresso Tests.

Now, you have the folder to write your Ui-related tests. First, you must ensure you are also following the best practices while writing the code for your compose UI. I will only suggest one thing.

Note: The UI composable function that you want to test should only have States and Events as parameters, and you are good to go.

Example:

@Composable
fun SessionFeedback(
 feedbackQuesState: UiState<FeedbackQuesDTO>,
 onBack: () -> Unit,
 onSubmit: (answers: List<Answer>) -> Unit
){}

Setup:

  1. Create a class for composable you want to test:
class SessionFeedBackTest {}

2. Add the given annotation to it.

@RunWith(AndroidJUnit4::class)

3. Add a Compose rule that we will use to test the components in the UI.

@get:Rule
val composeTestRule = createComposeRule()

4. Start writing your test functions…

Case 1:

If you are using some static(not passing them through params) things in your UI, then you can just verify if they are being shown or not.

Example: Here I am using an Image that will be always there irrespective of the parameters I am passing in my compose function.

  1. I will add a content description to that Image composable(it will be like naming the composable).
Image(
    painter = painterResource(id = R.drawable.feedback1),
    contentDescription = "feedback image"
)

Note: If your composable does not have a contentDescription param by default then still you can add a description using the:

Modifier.semantics { contentDescription = "Some desc" }
or
Modifier.testTag("")

2. In my test case I will just check if there is any object (here in compose we call them Node) that is present in the UI with the content description as what I added to my image.

3. Start writing your test function…

@Test
fun feedbackImagePresent() {
    val feedbackQuesDTO = FeedbackQuesDTO(
        questions = listOf()
    )//Creating dummy data for compose function parameters

    composeTestRule.setContent {//This will start showing the UI
        AppTheme {//Not mandatory, but just to check if theme is consitent or not.
            SessionFeedback(
                feedbackQuesState = UiState.Success(feedbackQuesDTO),//Passing Success UiState as only then we are showing UI in my case
                onBack = {},
                onSubmit = {}
            )
        }
    }
    composeTestRule
        .onNodeWithContentDescription("feedback image")
        .assertExists()
}

4. Here we are finding any Node that exists in the UI with its contentDescription.

Alternatively, you can also use these methods,

onNodeWithTag("")
onNodeWithText("")

5. Instead of just asserting if not exist or not, you can also assert for multiple things according to the compose function you are having.

Case X:

If you have dynamic data, that means it depends on the parameters you are passing or your data is changing with some events occurring in your UI, then you have to mock the same thing with the states and UI events that you are passing.

Example: If you are showing a no. in your UI and onClick of some button that no. is incremented then, In your test function…

  1. Create a variable and pass it in your function.

  2. Perform the respective onUiEvent to change your variable.

Note: If the component you are testing is present in a LazyList item then it will be rendered dynamically and if not shown on the screen then will be considered as not present. So, you might face a Node not found issue.

To solve the above issue, you need to scroll in your LazyColumn to that particular index where your composable is present in the screen.

In my case,

composeTestRule
    .onNodeWithContentDescription("LazyColumn")
    .performScrollToIndex(2)

I wasted a lot of time on this issue, I hope this solves your problem, if not then maybe you are missing the useUnmergedTree: Boolean while finding your node.

Leave a comment if you are facing any issues or to suggest any improvements.

I hope you found this helpful. If yes, then do FOLLOW ‘Sagar Malhotra’ for more Android-related content.