Essentials of Composable UI Testing: Understanding Basic Component Isolation

Thaw Zin Toe
5 min readDec 23, 2023

--

In the last few years, making Android apps has changed a lot. Now, developers use declarative UI frameworks more, and Jetpack Compose is the newest one. Declarative UI frameworks give programmers tools to change how the system displays the user interface.

User interfaces that are created in a declarative way can use these control structures to be more dynamic than the commonly used imperative frameworks for developing apps on Android and iOS.

Jetpack is more powerful and easier to test and debug. However, testing with Jetpack Compose can still be challenging, especially for those new to the framework.

That’s why I gonna talk about Compose Layout UI testing everything I know.

Why Compose UI Testing is so Important

Testing UIs or screens is used to verify the correct behavior of your Compose code, improving the quality of your app by catching errors early in the development process. UI testing can give you so many benefits just like this

  1. Clear and Concise
  2. Independent
  3. Ensure Correct Behavior
  4. Repeatable
  5. Precise
  6. Fast
  7. Continuous Integration
Credit: Philipp Lackner Test cheat

Testing in Compose UI

UI tests simulate user actions on your app’s user interface and assert a visual outcome.

On Android, we use the Compose testing framework for that (Espresso for XML)

Warning: UI Tests have a high risk of becoming flaky tests. Sometimes it might be failed, sometimes it will be passed and sometimes it will provide unreliable results. That’s why you must choose the correct view matches that match unique UI components.

There are 3 Types of UI testing

  1. Isolated UI Test → tests a single UI component in isolation ( eg: stateless or reusable component for EmailField or PhoneNumberField).
  2. Integrated UI Test → tests how UI component interacts with other classes (such as ViewModel)
  3. End-to-End Test → tests complete user interaction flows, typically across multiple screens

Note: When you are using the Composable Screen, keep distracting ViewModel as much as you can. If you are not doing just like this, You need to mock ViewModel or Fake Data to test this component. This is not good.

How to know what to test

When thinking about writing a test, ask yourself these 4 questions to get a feeling for whether you should write a test or not.

  1. How important is it for the essential features to work properly?
  2. What value does it bring to the business?
  3. How hard is this code to understand?
  4. How probable is it that the code will be altered in the future?

If you struggle to decide, write one.

Composable UI Setup

we need to add the following dependencies to the build.gradle file of the module containing your UI tests:

Compose Test APIs:

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

Some people are doing Espresso testing but I lack of knowledge Espresso testing. 😅

Begin Your Journey

We have a simpleGreeting composable function.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier.testTag("GreetingText")
)
}

we want to test whether this text is showing or not on our screen.

Firstly you need to declare and create testRule.

@get:Rule
val composeTestRule = createAndroidTestRule(MainActivity::class.java)

// with test JUnit4 extension
@get: Rule
val composeTestRule = createComposeRule()

Then we will create a Test function. In this cases, please care about test naming Rule

Recommdend → “MethodName/behavior” + “Condition” + ExpectedOutcome

@Test
fun testGreetings_withCorrectName_showCorrectly() {
val expectedValue = "Android"
with(composeTestRule) {
setContent { Greeting(name = expectedValue) }
onNode(hasTestTag("GreetingText"), useUnmergedTree = true)
.assertIsDisplayed()
}
}

Let me explain

composeTestRule – a TestRule to test UI created with Compose
onNode – a finder
hasTestTag – a Matcher
useUnmergedTree – a parameter that controls the UI tree hierarchy
asssertIsDisplayed – an assertion

If you want to click some composable components, you can add
performClick – an action

CreateTestRule

The createTestRule function locates a UI element based on its semantic attributes, such as test tags, content descriptions, or a custom property of a Composable. It can access the entire semantic tree of the UI present on the screen.

Finders

Finders search for the Composable that matches specific criteria and return a SemanticsNodeInteraction containing the Composable and its children if any exist.

Some common finders include:

  • onNode: Searches for a single Composable that matches. Throws an exception if more than one matching Composable is found.
  • onNodeWithTag: Searches for a single Composable with the test tag.
  • onNodeWithText: Searches for a single Composable with the specified text. A localized string can be retrieved using androidComposeTestRule.activity.getString(R.string.*).
  • onAllNodes: Looks for all nodes with matching and returns a non-iterable SemanticsNodeInteractionCollection containing found Composables and their possible children.

Matchers

A matcher defines the criteria a finder uses to locate the Composable. For instance:

  • hasContentDescription: Verifies that the Composable has the specified content description.
  • hasTestTag: Verifies that the Composable has the specified test tag.
  • isRoot: Verifies that it is the root Composable. etc …

There are also Hierarchical matchers and Selectors.

Hierarchical matchers → check the position of the Composable in the UI tree using methods like hasParent() or hasAnyChild().

Selectors → can identify Composables around and filter them.

For example, given the following tree:

|- Root composable
|- ButtonOne
|- ButtonTwo
|- ButtonThree

Calling onSiblings() on ButtonTwo will return ButtonOne and ButtonThree Composables.

useUnmergedTree

Since the Compose layout flattens its UI tree, some UI elements may be combined into a single Composable. For instance, two texts can be merged into a single Text Composable, leading to the potential loss of semantics. To inspect an intact UI tree, useUnmergedTree should be set to true.

Assertions

Assertions verify that the Composable meets specific conditions. Common assertions include:

  • assertExists
  • assertIsEnabled
  • assertTextEquals
  • assertContentDescription etc…

By using the generic assert(), you can supply your matcher and verify that it is satisfied for this node.

Actions

Actions simulate user events on Composables, such as:

  • performClick
  • performScroll
  • performTextInput etc …

They also support various types of gestures.

If you want to know more about more complex UI Testing, you can look

from the Android.

I will cover in next topics because Composable UI testing is so large and wide area to cover.

Please start your composable testing and have a fun part of using this. When you are struggling to write a test, you can freely contact to me. I am happy to serve it

Thank you for reading 😃

--

--