Search code examples
androidandroid-jetpack-composeui-testing

Add intent extras in compose UI test


I want to UI test an Activity that uses Jetpack Compose. The docs provide some information on how to test such a screen with two variants:

 @get:Rule val composeTestRule = createComposeRule()

if I don't need the activity itself to run and want to test just my composables or

 @get:Rule val composeTestRule = createAndroidComposeRule<MyActivity>()

if I do need the activity.

In the second case, how can I pass an Intent with Extras to the activity?

I've tried:

@Before
fun setUp() {
    composeTestRule.activity.intent = Intent().apply {
        putExtra(
            "someKey",
            123
        )
    }
}

but the intent extras are still null in the activity.


Solution

  • The issue with setting composeTestRule.activity.intent in setUp() is that the Activity is already created at that point and the Activity's OnCreate was already called. So your intent properties that you're setting in setUp() are being set but it's too late to be consumed in Activity.OnCreate.

    Unfortunately Google doesn't create a helper method like they do with createAndroidComposeRule<MyActivity>(), yet. However it's possible to write a helper method to work around:

    Option 1 (An intent per test)

    How to use

    class MyActivityTest {
    
        @get:Rule
        val composeRule = createEmptyComposeRule()
    
        @Test
        fun firstTimeLogIn() = composeRule.launch<MyActivity>(
            onBefore = {
                // Set up things before the intent
            },
            intentFactory = {
                Intent(it, MyActivity::class.java).apply {
                    putExtra("someKey", 123)
                }
            },
            onAfterLaunched = {
                // Assertions on the view 
                onNodeWithText("Username").assertIsDisplayed()
            })
    }
    

    Kotlin Extension Helper Method

    import android.app.Activity
    import android.content.Context
    import android.content.Intent
    import androidx.compose.ui.test.junit4.ComposeTestRule
    import androidx.compose.ui.test.junit4.createEmptyComposeRule
    import androidx.test.core.app.ActivityScenario
    import androidx.test.core.app.ApplicationProvider
    
    /**
    * Uses a [ComposeTestRule] created via [createEmptyComposeRule] that allows setup before the activity
    * is launched via [onBefore]. Assertions on the view can be made in [onAfterLaunched].
    */
    inline fun <reified A: Activity> ComposeTestRule.launch(
        onBefore: () -> Unit = {},
        intentFactory: (Context) -> Intent = { Intent(ApplicationProvider.getApplicationContext(), A::class.java) },
        onAfterLaunched: ComposeTestRule.() -> Unit
    ) {
        onBefore()
    
        val context = ApplicationProvider.getApplicationContext<Context>()
    
        ActivityScenario.launch<A>(intentFactory(context)).use {
            onAfterLaunched()
        }
    }
    

    Option 2 (Single intent for each test class)

    How to use

    @RunWith(AndroidJUnit4::class)
    class MyActivityTest {
    
        @get:Rule
        val composeTestRule = createAndroidIntentComposeRule<MyActivity> {
            Intent(it, MyActivity::class.java).apply {
                putExtra("someKey", 123)
            }
        }
    
        @Test
        fun Test1() {
    
        }
    
    }
    

    Kotlin Extension Helper Method

    import android.content.Context
    import android.content.Intent
    import androidx.activity.ComponentActivity
    import androidx.compose.ui.test.junit4.AndroidComposeTestRule
    import androidx.test.core.app.ApplicationProvider
    import androidx.test.ext.junit.rules.ActivityScenarioRule
    
    /**
    * Factory method to provide Android specific implementation of createComposeRule, for a given
    * activity class type A that needs to be launched via an intent.
    *
    * @param intentFactory A lambda that provides a Context that can used to create an intent. A intent needs to be returned.
    */
    inline fun <A: ComponentActivity> createAndroidIntentComposeRule(intentFactory: (context: Context) -> Intent) : AndroidComposeTestRule<ActivityScenarioRule<A>, A> {
        val context = ApplicationProvider.getApplicationContext<Context>()
        val intent = intentFactory(context)
    
        return AndroidComposeTestRule(
            activityRule = ActivityScenarioRule(intent),
            activityProvider = { scenarioRule -> scenarioRule.getActivity() }
        )
    }
    
    /**
    * Gets the activity from a scenarioRule.
    *
    * https://androidx.tech/artifacts/compose.ui/ui-test-junit4/1.0.0-alpha11-source/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt.html
    */
    fun <A : ComponentActivity> ActivityScenarioRule<A>.getActivity(): A {
        var activity: A? = null
    
        scenario.onActivity { activity = it }
    
        return activity ?: throw IllegalStateException("Activity was not set in the ActivityScenarioRule!")
    }