Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

14. UI Tests in Jetpack Compose
Written by Prateek Prasad

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Congratulations on wrapping up the JetReddit app. Throughout the last thirteen chapters, you learned about the fundamentals of Jetpack Compose and built the JetReddit app from the ground up.

It’s time to verify the app’s functionality and learn how UI testing works in Jetpack Compose. In this chapter, you’ll learn how to write UI tests for your screens and components in Jetpack Compose and how assertions work. Let’s jump straight in!

Loading the Starter Project

To follow along with the code examples, open this chapter’s starter project in Android Studio and select Open an existing project.

Next, navigate to 14-ui-tests-in-compose/projects and select the starter folder as the project root. Once the project opens, let it build and sync, and you’re ready to go!

It’s the same app you’ve worked on, with a few structural changes to the code.

Open the JetRedditApp.kt file. Here, the ViewModel is no longer passed as a parameter to JetRedditApp composable. Instead, you now have separate state objects and event handlers to render the UI and pass events to the parent activity hosting the composable:

@Composable
fun JetRedditApp(
    allPosts: List<PostModel>,
    myPosts: List<PostModel>,
    communities: List<String>,
    selectedCommunity: String,
    savePost: (post: PostModel) -> Unit,
    searchCommunities: (searchedText: String) -> Unit,
    communitySelected: (community: String) -> Unit,
) {
    JetRedditTheme {
        AppContent(
            allPosts,
            myPosts,
            communities,
            selectedCommunity,
            savePost,
            searchCommunities,
            communitySelected
        )
    }
}

This slight change in structure will go a long way in helping you set up the test environment in the absence of a proper dependency injection setup.

The dependencies required for testing your composables have been added to the app’s build.gradle file as shown below:

  // Compose testing dependencies
  androidTestImplementation "androidx.compose.ui:ui-test:$compose_version"
  androidTestImplementation "androidx.compose.
  ui:ui-test-junit4:$compose_version"
  debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"

Before writing your first UI test, it’s good to understand how tests work in Jetpack Compose.

Note: Please ensure you use an emulator or device running a modern version of Android before running the tests in this chapter. Some older versions of Android may report failing tests.

Behind the Scenes of UI Tests in Jetpack Compose

UI tests for composables fall into the instrumentation test category, just like espresso tests, as you need a physical device or an emulator to run them.

Testing UI Components in Jetpack Compose

Just like previewing and deploying individual components in Jetpack Compose, you can also write UI tests for them in isolation. Testing components in isolation allows you the ease and flexibility to set up a contained environment to test individual UI variations quickly.

@get:Rule(order = 0)
val composeTestRule = createComposeRule()
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
@Test
fun title_is_displayed() {
  val post = PostModel.DEFAULT_POST
  composeTestRule.setContent {
      Post(post = post)
  }

  composeTestRule.onNodeWithText(post.title).assertIsDisplayed()
  }
import com.yourcompany.android.jetreddit.components.Post
import com.yourcompany.android.jetreddit.domain.model.PostModel
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText

@Test
fun like_count_is_displayed() {
  val post = PostModel.DEFAULT_POST
  composeTestRule.setContent {
      Post(post = post)
  }
  composeTestRule.onNodeWithText(post.likes).assertIsDisplayed()
}
modifier = Modifier
  .fillMaxWidth()
  .aspectRatio(painter.intrinsicSize.width / painter.intrinsicSize.height)
  .testTag(Tags.POST_IMAGE)
import androidx.compose.ui.platform.testTag
import com.yourcompany.android.jetreddit.util.Tags
@Test
fun image_is_displayed_for_post_with_image() {
  val post = PostModel.DEFAULT_POST
  composeTestRule.setContent {
      ImagePost(post = post)
  }

  composeTestRule.onNodeWithTag(Tags.POST_IMAGE, true).assertIsDisplayed()
}
import androidx.compose.ui.test.onNodeWithTag
import com.yourcompany.android.jetreddit.components.ImagePost
import com.yourcompany.android.jetreddit.util.Tags

Writing Tests for Screens

Having covered component-level tests, it’s now time to look at how UI testing works for screens.

@get:Rule(order = 0)
val composeTestRule = createAndroidComposeRule(MainActivity::class.java)
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import org.junit.Rule
@Test
fun app_shows_home_screen() {
  composeTestRule.activity.setContent { //1
      JetRedditApp(
          allPosts = PostDataFactory.createPosts(), //2
          myPosts = PostDataFactory.createPosts(),
          communities = PostDataFactory.createCommunities(),
          selectedCommunity = PostDataFactory.randomString(),
          savePost = {},
          searchCommunities = {},
          communitySelected ={}
      )
  }

  //3
  composeTestRule.onNodeWithText(
    composeTestRule.activity.getString(R.string.home)
  ).assertIsDisplayed()
}
import androidx.activity.compose.setContent
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import com.yourcompany.android.jetreddit.factory.PostDataFactory
BottomNavigation(modifier = modifier) {
  items.forEach {
    BottomNavigationItem(
      modifier = Modifier.testTag(it.screen.route),

	            ....

	)
   }
}
import androidx.compose.ui.platform.testTag
@Test
fun app_shows_subreddits_screen() {
   composeTestRule.activity.setContent { //1
       JetRedditApp(
           allPosts = PostDataFactory.createPosts(),
           myPosts = PostDataFactory.createPosts(),
           communities = PostDataFactory.createCommunities(),
           selectedCommunity = PostDataFactory.randomString(),
           savePost = {},
           searchCommunities = {},
           communitySelected ={}
       )
   }

   composeTestRule.onNodeWithTag(
       Screen.Subscriptions.route
   ).performClick() //2

   composeTestRule.onNodeWithText(
    composeTestRule.activity.getString(R.string.subreddits)
  ).assertIsDisplayed() //3
}
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.yourcompany.android.jetreddit.routing.Screen

navigationIcon = {
 IconButton(
  modifier = Modifier.testTag(Tags.ACCOUNT_BUTTON),
  onClick = {
  coroutineScope.launch { scaffoldState.drawerState.open() }
 }) {
         			...
 }
}
import com.yourcompany.android.jetreddit.util.Tags
@Test
fun app_shows_drawer() {

  composeTestRule.activity.setContent {
      JetRedditApp(
          allPosts = PostDataFactory.createPosts(),
          myPosts = PostDataFactory.createPosts(),
          communities = PostDataFactory.createCommunities(),
          selectedCommunity = PostDataFactory.randomString(),
          savePost = {},
          searchCommunities = {},
          communitySelected ={}
      )
  }

  composeTestRule.onNodeWithTag(
      Tags.ACCOUNT_BUTTON
  ).performClick()

  composeTestRule.onNodeWithText(
    composeTestRule.activity.getString(R.string.default_username)
  ).assertIsDisplayed()

}
import com.yourcompany.android.jetreddit.util.Tags

@Composable
fun JoinButton(onClick: (Boolean) -> Unit = {}) {
  ...
  ...
  Box(
      modifier = Modifier
        .clip(shape)
        ...
        ...
        .testTag(Tags.JOIN_BUTTON) // add here
  ) {
    ...
    ...
  }
}
import androidx.compose.ui.platform.testTag
import com.yourcompany.android.jetreddit.util.Tags
Box(
 modifier = Modifier
  .align(Alignment.BottomCenter)
  .padding(bottom = 16.dp)
  .testTag(Tags.JOINED_TOAST)
) {
 JoinedToast(visible = isToastVisible)
}
import androidx.compose.ui.platform.testTag
import com.yourcompany.android.jetreddit.util.Tags
@Test
fun app_shows_toast_when_joining_community() {
  composeTestRule.activity.setContent {
      JetRedditApp(
          allPosts = PostDataFactory.createPosts(),
          myPosts = PostDataFactory.createPosts(),
          communities = PostDataFactory.createCommunities(),
          selectedCommunity = PostDataFactory.randomString(),
          savePost = {},
          searchCommunities = {},
          communitySelected ={}
      )
  }

  composeTestRule.onAllNodes(
      hasTestTag(Tags.JOIN_BUTTON)
  ).onFirst().performClick()

  composeTestRule.onNodeWithTag(Tags.JOINED_TOAST).assertIsDisplayed()
}

Writing Tests for Hybrid Screens

In a hybrid setup you will often find composables inside an XML view hierarchy and views inside composable trees.

if (screen == Screen.Home) {
    IconButton(
      modifier = Modifier.testTag(Tags.CHAT_BUTTON), // add here
      onClick = {
        context.startActivity(
          Intent(context, ChatActivity::class.java)
        )
      }) {

              ...

     }
}
@Test
fun chat_button_is_displayed() {

  //1
  composeTestRule.onNodeWithTag(
    Tags.CHAT_BUTTON
  ).performClick()

  //2
  Espresso.onView(withId(R.id.composeButton))
    .check(matches(isDisplayed()))
}
import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId

@get:Rule
val composeTestRule = createComposeRule()
@Test
fun trending_item_is_displayed() {
    val topic = TrendingTopicModel(
        "Compose Tutorial",
        R.drawable.jetpack_composer
    )

    composeTestRule.setContent {
        TrendingTopic(topic)
    }

    composeTestRule.onNodeWithTag(Tags.TRENDING_ITEM)
      .assertIsDisplayed()
}
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import com.yourcompany.android.jetreddit.util.Tags
import org.junit.Rule
import com.yourcompany.android.jetreddit.screens.TrendingTopic
import com.yourcompany.android.jetreddit.screens.TrendingTopicModel
  AndroidView(modifier = Modifier.testTag(Tags.TRENDING_ITEM), factory = { context ->
    TrendingTopicView(context).apply {
      text = trendingTopic.text
      image = trendingTopic.imageRes
    }
  })

Key Points

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now