Exploring Compose Test Rules

When it comes to testing Composables, we can utilise the ComposeContentTestRule to compose, interact with and perform assertions on our composables. However, there are multiple ways to create a compose rule and in this blog post, I want to share what each one can be used for we can learn which one we’ll need for our different testing scenarios.


Compose Rule

Purpose: Compose content and test it in isolation

The first of them is the Compose Rule, which is created using the createComposeRule function. This rule is designed for testing composables in isolation and probably going to be your go-to compose rule in most compose-only scenarios.

So for example, let’s say you want to write tests for a compose-only feature or write component-level tests for your composables. In these cases, you are only interacting directly with Compose – this is exactly what the Compose Rule is designed for.

Let’s take a quick look at an example of how we can use this rule. Let’s say we have the following composable:

@Composable
fun ContentItem(
    modifier: Modifier = Modifier
) {
    Card(modifier = modifier.testTag("TAG_CONTAINER")) {
        Text(text = "Hello")
    }
}

When it comes to testing this composable, we’re going to want to compose the ContentItem, followed by performing any desired interactions/assertions on it. We can start by declaring the Compose Rule, followed by using this to set the composed content, and finally locating the desired node to assert that it is displayed.

@get:Rule
val composeRule = createComposeRule()

@Test
fun My_Test() {
    composeRule.setContent {
        ContentItem()
    }

    composeRule.onNodeWithTag("TAG_CONTAINER")
        .assertIsDisplayed()
}

Empty Compose Rule

Purpose: Create your own compose host during tests without the need to compose any content

Next up we have the Empty Compose Rule, this rule allows us to access composables without the need to manually compose content from within our tests.

One example would be an activity containing Composables that we wish to test. For example, let’s imagine our MainActivity class containing initialisation logic outside of our composable. In this case, we won’t be able to use the createComposeRule function as we have a tight coupling to our activity. However, we can use the createEmptyComposeRule to create an alternative version of the rule that does not require us to set composable content, but will still allow us to access the node tree and in turn, our composables.

@get:Rule
val composeRule = createEmptyComposeRule()

private lateinit var scenario: ActivityScenario<MainActivity>

@Before
fun setup() {
    scenario = ActivityScenario.launch(MainActivity::class.java)
}

@Test
fun My_Test() {
    composeRule.onNodeWithTag("TAG_CONTAINER")
        .assertIsDisplayed()
}

As noted by Sam Edwards, this rule can also be used to access composables from existing Espresso tests. This is helpful as during compose migration, it’s likely you’ll be used the ComposeView to slot composables into existing Android Views/Layouts. When doing this, Espresso cannot access the content of these composables, which can make it difficult to write tests. When in this situation, we can use the empty compose rule to access our composables that are composed inside of Android layouts, in the same way that we would in the example shown above.

@get:Rule
val composeRule = createEmptyComposeRule()

private lateinit var scenario: ActivityScenario<MainActivity>

@Before
fun setup() {
    scenario = ActivityScenario.launch(MainActivity::class.java)
}

@Test
fun My_Test() {
    onView(withId(R.id.button)).perform(click())
    onView(withId(R.id.my_view)).check(matches(isDisplayed()))

    composeRule.onNodeWithTag("TAG_CONTAINER").assertIsDisplayed()
}

Android Compose Rule

Purpose: Compose content within custom activities

Finally, we have the Android Compose rule. This allows us to compose content inside a custom activity. Similar to the scenario mentioned above, we may have a custom activity that we are using as a container for our composable. This could be for any reason – due to the architecture of your app or maybe during migration to compose, using a custom activity. In this cases, we may see examples such as an activity hosting a viewmodel which configures the state of the screen, which is then used to compose UI. Regardless of why, this test rule allows you to set the composable content of the activity while also allowing you to keep any existing activity logic in place.

This is different from the first compose rule as at this point, we are no longer testing our composable in isolation. This becomes more of a screen-based (integration) test to ensure that our composable is integrated correctly with its hosting activity.

When it comes to using the android compose rule, the main difference is that the compose rule reference now gives us access to an activity reference. We can use this activity reference to call the setContent function to compose our UI. Then, we can interact with the node tree just like we would for the other compose rules.

@get:Rule
val composeRule = createAndroidComposeRule<MainActivity>()

@Test
fun My_Test() {
    composeRule.activity.setContent {
        ContentItem()
    }

    composeRule.onNodeWithTag("TAG_CONTAINER")
        .assertIsDisplayed()
}

In this blog post, we’ve taken a quick look at the three Compose Rules that are available to use when testing our composables. From isolated component testing to integration testing, these rules allow us to test our composables in a range of different scenarios.

How are you using these compose rules? If there are any other scenarios that you’ve encountered, I’d love to hear about them!

Leave a Reply

Your email address will not be published. Required fields are marked *