I want to achieve something like this:
// NavigateToUserAccountTest.kt
@ExperimentalMaterialApi
@HiltAndroidTest
class NavigateTest {
@EarlyEntryPoint
@InstallIn(SingletonComponent::class)
internal interface HiltTestEntryPoint {
val rule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>
}
@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val rule = createAndroidComposeRule<MainActivity>() = EarlyEntryPoints.get(ApplicationProvider.getApplicationContext(), HiltTestEntryPoint::class.java).rule
@Inject
lateinit var bottomNavigation : BottomNavigation
@Before
fun init() {
hiltRule.inject()
}
@Test
fun userAccountTab_is_displayed() {
BottomNavigation.userAccountTab.assertIsDisplayed()
}
@Test
fun shoppingTab_is_displayed{
BottomNavigation.shoppingTab.assertIsDisplayed()
}
}
// BottomNavigation.kt
class BottomNavigation @Inject constructor(
) {
private val rule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity> = EarlyEntryPoints.get(ApplicationProvider.getApplicationContext(), HiltTestEntryPoint::class.java).rule
val userAccountTab get() = rule.onNode(hasText(rule.activity.getString(R.string.nav_user_account)))
val shoppingTab get() = rule.onNode(hasText(rule.activity.getString(R.string.nav_shopping)))
}
Basically I want to encapsulate SemanticsMatcher
inside a class so that class contains the same SemanticsMatcher
that relate to the same screen. For example, BottomNavigation has userAccountTab. So that I can reuse this class injected into different tests. My current solution is to manually instantiate BottomNavigation by passing rule as the contructor parameter. I want to avoid those boilerplate code by using Hilt injection.
I tried to create a module like this
@Module
@InstallIn(SingletonComponent::class)
class UITestModule {
@Provides
@Singleton
fun provideMainActivityAndroidComposeRule() = createAndroidComposeRule<MainActivity>()
}
and change rule to
@Inject
@get:Rule
lateinit var rule: AndroidComposeTestRule<ActivityScenarioRule<MainActivity>, MainActivity>
But this does not work because injection only happens when call hiltRule.inject()
.
Is it possible to inject rule into other classes?
Update:
So I am so close to make it working, by using EarlyEntryPoint. But on the second test, it gives an exception below.
java.lang.IllegalStateException: Only a single call to `runTest` can be performed during one test.
at kotlinx.coroutines.test.TestScopeImpl.enter(TestScope.kt:220)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest(TestBuilders.kt:165)
at kotlinx.coroutines.test.TestBuildersKt.runTest(Unknown Source:1)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt.runTest$default(TestBuilders.kt:161)
at kotlinx.coroutines.test.TestBuildersKt.runTest$default(Unknown Source:1)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment.withTestCoroutines(ComposeUiTest.android.kt:342)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment.access$withTestCoroutines(ComposeUiTest.android.kt:218)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment$runTest$1$1$1.invoke(ComposeUiTest.android.kt:294)
at androidx.compose.ui.test.junit4.EspressoLink.withStrategy(EspressoLink.android.kt:66)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment$runTest$1$1.invoke(ComposeUiTest.android.kt:293)
at androidx.compose.ui.test.junit4.IdlingResourceRegistry.withRegistry(IdlingResourceRegistry.jvm.kt:157)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment$runTest$1.invoke(ComposeUiTest.android.kt:292)
at androidx.compose.ui.test.junit4.ComposeRootRegistry.withRegistry(ComposeRootRegistry.android.kt:146)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment.runTest(ComposeUiTest.android.kt:291)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule$apply$1.evaluate(AndroidComposeTestRule.android.kt:147)
at dagger.hilt.android.internal.testing.MarkThatRulesRanRule$1.evaluate(MarkThatRulesRanRule.java:108)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runners.Suite.runChild(Suite.java:128)
at org.junit.runners.Suite.runChild(Suite.java:27)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at org.junit.runner.JUnitCore.run(JUnitCore.java:115)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:67)
at androidx.test.internal.runner.TestExecutor.execute(TestExecutor.java:58)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:446)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2278)
``
EarlyEntryPoints.get(ApplicationProvider.getApplicationContext(), HiltTestEntryPoint::class.java).rule
returns the same AndroidComposeTestRule
in the test and in BottomNavigation
. I don't what is wrong with that... I am stuck.