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 ComposeTestRule
: createComposeRule
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.
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