9

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.

fweigl
  • 21,278
  • 20
  • 114
  • 205

3 Answers3

6

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

    onAfterLaunched()
}

Option 2 (Single intent for each test)

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!")
}
hippietrail
  • 15,848
  • 18
  • 99
  • 158
ryanholden8
  • 536
  • 4
  • 13
  • The issue with this approach is you can't set an Intent per test. It's the same intent for the whole class. Is this correct? – Maragues May 03 '22 at 14:59
  • 2
    @Maragues - Good point. I updated to include an option that uses an extension method that sets up an intent for each test. – ryanholden8 May 23 '22 at 16:16
3
class ActivityTest {

    private lateinit var scenario: ActivityScenario<Activity>

    @get:Rule
    val composeAndroidRule = createEmptyComposeRule()


    @Before
    fun setUp() {

        scenario = ActivityScenario.launch(
            createActivityIntent(
                InstrumentationRegistry.getInstrumentation().targetContext,
            )
        )
    }

    private fun createActivityIntent(
        context: Context
    ): Intent {
        val intent = Intent(context, Activity::class.java)
        return intent
    }

}

hopefully, this code snippet would works for you

dsh
  • 12,037
  • 3
  • 33
  • 51
Oscar Ivan
  • 819
  • 1
  • 11
  • 21
  • While it works, emptyComposeRule uses `ComponentActivity` under the hood, not the activity under test. I still don't know the implications, tho. – Maragues May 03 '22 at 14:58
  • 1
    I found something on the issue tracker. It looks like yours is the recommended way https://issuetracker.google.com/issues/175647572 and https://issuetracker.google.com/issues/174472899 – Maragues May 03 '22 at 15:02
-3

This is an easy solution:

@Before
fun setUp() {
    composeTestRule.activity.intent.apply {
        putExtra(
            "someKey",
            123
        )
    }
}
sanya5791
  • 433
  • 4
  • 5
  • This is the same code mentioned in question as something they have tried and did not work. – ryanholden8 Dec 19 '22 at 21:02
  • @ryanholden8, it's a different solution from original and it works for me. The original solution creates a NEW intent, but this solution applies extra parameters to already existing intent. – sanya5791 Dec 23 '22 at 15:59
  • When are you consuming the intent in your code? Within the Activity's OnCreate method? or at a later time? – ryanholden8 Dec 23 '22 at 16:29