Blog Infos
Author
Published
Topics
, , ,
Published
Source: https://unsplash.com/photos/turned-on-android-smartphone-t8TOMKe6xZU

 

Before we talk about how to assert intent data consumption in instrumented tests, we need to talk about why it’s crucial.

Consider a scenario where your composables access an intent extra. You might wanna verify that your composables can display the value coming from the intent correctly.

val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "no extra text"
Text(name = text)

What’s more likely to happen in real-world scenarios is that your Activity will access the intent extras, pass them over to ViewModel and ViewModel will compute based on the input and set the UI state.

The question is, how do we assert that the intent extras are consumed correctly if we wanna avoid the high cost of manual testing?

If you Google for a solution, ComposeTestRule, provided by Jetpack, is most likely the first thing you’ll find. There are two main ways to create a ComposeTestRulecreateComposeRule and createAndroidComposeRule.

createAndroidComposeRule is the tool of choice if you want to launch your own Activity, but createAndroidComposeRule takes only two parameters, as you can see in the screenshot below taken from the Jetpack’s official Compose test library.

A Function From Official Compose Test Library

 

This means there’s no straightforward way to launch an Activity with your own intent using the tools provided by Jetpack in order to assert composables in the Activity.

That’s the problem ComposeUiTest solves. This library uses the official Compose test library under the hood and provides easy-to-use functions for developer friendliness.

Let’s take a look at how easy it is to use the library.

First, import the library.

dependencies {
    androidTestImplementation("io.github.aungthiha:compose-ui-test:1.0.1")
}

In your test case, you can pass in your intent to start an Activity and in the trailing lambda, you can assert your composables.

Gists for medium article titled Jetpack Compose: Assert Intent Data Consumption in Instrumented Tests

fun <A : ComponentActivity> createAndroidComposeRule(
startActivityIntent: Intent
): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
activityRule = ActivityScenarioRule(startActivityIntent),
activityProvider = ::getActivityFromTestRule
)
runAndroidComposeUiTest(
activityLauncher = {
ActivityScenario.launch<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value"),
Bundle().apply {
putString("key", "value")
}
)
}
) {
// assert composables
}
runAndroidComposeUiTest<YourActivity>(
startActivityIntent = Intent(
ApplicationProvider.getApplicationContext(),
YourActivity::class.java
).putExtra("key", "value")
) {
// assert composables
// example assertion below
// onNodeWithText("hello").assertExists().assertIsDisplayed()
}
@get:Rule
val composeTestRule = createAndroidComposeRule<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value")
)
@ExperimentalTestApi
fun <A : ComponentActivity> runAndroidComposeUiTest(
activityLauncher: () -> ActivityScenario<A>,
effectContext: CoroutineContext = EmptyCoroutineContext,
block: AndroidComposeUiTest<A>.() -> Unit
) {
var scenario: ActivityScenario<A>? = null
val environment = AndroidComposeUiTestEnvironment(effectContext) {
requireNotNull(scenario) {
"ActivityScenario has not yet been launched, or has already finished. Make sure that " +
"any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
"is made within the lambda passed to AndroidComposeUiTestEnvironment.runTest()"
}.getActivity()
}
try {
environment.runTest {
scenario = activityLauncher()
block()
}
} finally {
scenario?.close()
}
}

You can also directly use ActivityScenario to launch an Activity the way you prefer.

Gists for medium article titled Jetpack Compose: Assert Intent Data Consumption in Instrumented Tests

fun <A : ComponentActivity> createAndroidComposeRule(
startActivityIntent: Intent
): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
activityRule = ActivityScenarioRule(startActivityIntent),
activityProvider = ::getActivityFromTestRule
)
runAndroidComposeUiTest(
activityLauncher = {
ActivityScenario.launch<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value"),
Bundle().apply {
putString("key", "value")
}
)
}
) {
// assert composables
}
runAndroidComposeUiTest<YourActivity>(
startActivityIntent = Intent(
ApplicationProvider.getApplicationContext(),
YourActivity::class.java
).putExtra("key", "value")
) {
// assert composables
// example assertion below
// onNodeWithText("hello").assertExists().assertIsDisplayed()
}
@get:Rule
val composeTestRule = createAndroidComposeRule<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value")
)
@ExperimentalTestApi
fun <A : ComponentActivity> runAndroidComposeUiTest(
activityLauncher: () -> ActivityScenario<A>,
effectContext: CoroutineContext = EmptyCoroutineContext,
block: AndroidComposeUiTest<A>.() -> Unit
) {
var scenario: ActivityScenario<A>? = null
val environment = AndroidComposeUiTestEnvironment(effectContext) {
requireNotNull(scenario) {
"ActivityScenario has not yet been launched, or has already finished. Make sure that " +
"any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
"is made within the lambda passed to AndroidComposeUiTestEnvironment.runTest()"
}.getActivity()
}
try {
environment.runTest {
scenario = activityLauncher()
block()
}
} finally {
scenario?.close()
}
}

But wait, what if you need to start all test cases with the same intent? Repeating runAndroidComposeUiTest in all the test cases would be cumbersome. For that, we can create a test rule using createAndroidComposeRule. Yes, it’s the same function name as the official function but it comes from the library.

Gists for medium article titled Jetpack Compose: Assert Intent Data Consumption in Instrumented Tests

fun <A : ComponentActivity> createAndroidComposeRule(
startActivityIntent: Intent
): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
activityRule = ActivityScenarioRule(startActivityIntent),
activityProvider = ::getActivityFromTestRule
)
runAndroidComposeUiTest(
activityLauncher = {
ActivityScenario.launch<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value"),
Bundle().apply {
putString("key", "value")
}
)
}
) {
// assert composables
}
runAndroidComposeUiTest<YourActivity>(
startActivityIntent = Intent(
ApplicationProvider.getApplicationContext(),
YourActivity::class.java
).putExtra("key", "value")
) {
// assert composables
// example assertion below
// onNodeWithText("hello").assertExists().assertIsDisplayed()
}
@get:Rule
val composeTestRule = createAndroidComposeRule<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value")
)
@ExperimentalTestApi
fun <A : ComponentActivity> runAndroidComposeUiTest(
activityLauncher: () -> ActivityScenario<A>,
effectContext: CoroutineContext = EmptyCoroutineContext,
block: AndroidComposeUiTest<A>.() -> Unit
) {
var scenario: ActivityScenario<A>? = null
val environment = AndroidComposeUiTestEnvironment(effectContext) {
requireNotNull(scenario) {
"ActivityScenario has not yet been launched, or has already finished. Make sure that " +
"any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
"is made within the lambda passed to AndroidComposeUiTestEnvironment.runTest()"
}.getActivity()
}
try {
environment.runTest {
scenario = activityLauncher()
block()
}
} finally {
scenario?.close()
}
}

That’s all there is! That’s how easy it is to use the library.

Curious how the library internally works?

The runAndroidComposeUiTest function is written by referencing its counterpart from the Jetpack’s official Compose test library.

In a nutshell, the runAndroidComposeUiTest utilizes AndroidComposeUiTestEnvironment to run the test and provide AndroidComposeUiTest to the trailing lambda(block) to perform assertions on composables. AndroidComposeUiTestEnvironment does not hold reference to the Activity; instead, it uses the ActivityScenario created by the parameter activityLauncher to retrieve the Activity instance whenever it needs.

Gists for medium article titled Jetpack Compose: Assert Intent Data Consumption in Instrumented Tests

fun <A : ComponentActivity> createAndroidComposeRule(
startActivityIntent: Intent
): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
activityRule = ActivityScenarioRule(startActivityIntent),
activityProvider = ::getActivityFromTestRule
)
runAndroidComposeUiTest(
activityLauncher = {
ActivityScenario.launch<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value"),
Bundle().apply {
putString("key", "value")
}
)
}
) {
// assert composables
}
runAndroidComposeUiTest<YourActivity>(
startActivityIntent = Intent(
ApplicationProvider.getApplicationContext(),
YourActivity::class.java
).putExtra("key", "value")
) {
// assert composables
// example assertion below
// onNodeWithText("hello").assertExists().assertIsDisplayed()
}
@get:Rule
val composeTestRule = createAndroidComposeRule<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value")
)
@ExperimentalTestApi
fun <A : ComponentActivity> runAndroidComposeUiTest(
activityLauncher: () -> ActivityScenario<A>,
effectContext: CoroutineContext = EmptyCoroutineContext,
block: AndroidComposeUiTest<A>.() -> Unit
) {
var scenario: ActivityScenario<A>? = null
val environment = AndroidComposeUiTestEnvironment(effectContext) {
requireNotNull(scenario) {
"ActivityScenario has not yet been launched, or has already finished. Make sure that " +
"any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
"is made within the lambda passed to AndroidComposeUiTestEnvironment.runTest()"
}.getActivity()
}
try {
environment.runTest {
scenario = activityLauncher()
block()
}
} finally {
scenario?.close()
}
}

The overloaded runAndroidComposeUiTest that takes in an intent uses the one that takes in activityLauncher internally, which is explained above.

As for createAndroidComposeTestRule, it initializes an instance of AndroidComposeTestRule using an ActivityScenarioRule created with the provided intent. It returns the initialized AndroidComposeTestRule instance, which is then available for use in performing assertions on composables.

Gists for medium article titled Jetpack Compose: Assert Intent Data Consumption in Instrumented Tests

fun <A : ComponentActivity> createAndroidComposeRule(
startActivityIntent: Intent
): AndroidComposeTestRule<ActivityScenarioRule<A>, A> = AndroidComposeTestRule(
activityRule = ActivityScenarioRule(startActivityIntent),
activityProvider = ::getActivityFromTestRule
)
runAndroidComposeUiTest(
activityLauncher = {
ActivityScenario.launch<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value"),
Bundle().apply {
putString("key", "value")
}
)
}
) {
// assert composables
}
runAndroidComposeUiTest<YourActivity>(
startActivityIntent = Intent(
ApplicationProvider.getApplicationContext(),
YourActivity::class.java
).putExtra("key", "value")
) {
// assert composables
// example assertion below
// onNodeWithText("hello").assertExists().assertIsDisplayed()
}
@get:Rule
val composeTestRule = createAndroidComposeRule<YourActivity>(
Intent(ApplicationProvider.getApplicationContext(), YourActivity::class.java)
.putExtra("key", "value")
)
@ExperimentalTestApi
fun <A : ComponentActivity> runAndroidComposeUiTest(
activityLauncher: () -> ActivityScenario<A>,
effectContext: CoroutineContext = EmptyCoroutineContext,
block: AndroidComposeUiTest<A>.() -> Unit
) {
var scenario: ActivityScenario<A>? = null
val environment = AndroidComposeUiTestEnvironment(effectContext) {
requireNotNull(scenario) {
"ActivityScenario has not yet been launched, or has already finished. Make sure that " +
"any call to ComposeUiTest.setContent() and AndroidComposeUiTest.getActivity() " +
"is made within the lambda passed to AndroidComposeUiTestEnvironment.runTest()"
}.getActivity()
}
try {
environment.runTest {
scenario = activityLauncher()
block()
}
} finally {
scenario?.close()
}
}

The library hides all of these details from developers, allowing them to concentrate on writing the actual test cases.

If you find this article and the library valuable, a round of applause would be much appreciated. Please, feel free to leave a comment if anything needs clarification or if you have additional insights. Your feedback fuels my commitment to enhancing my blogging skills and supporting fellow developers.

I hope you enjoyed the article! Happy coding!

This article was previously published on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

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

Fill out this field
Fill out this field
Please enter a valid email address.

Menu