cucumber / cucumber-android

Android support for Cucumber-JVM
MIT License
130 stars 62 forks source link

ActivityScenario is null when executing the test #126

Closed dawidhyzy closed 6 months ago

dawidhyzy commented 6 months ago

I get a null pointer exception when executing the tests. If I write the same test without Cucumber and use plain AndroidJUnitRunner the test as supposed to

πŸ‘“ What did you see?

java.lang.NullPointerException
at androidx.test.internal.util.Checks.checkNotNull(Checks.java:38)
at androidx.test.ext.junit.rules.ActivityScenarioRule.getScenario(ActivityScenarioRule.java:125)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule_androidKt.getActivityFromTestRule(AndroidComposeTestRule.android.kt:345)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule_androidKt.access$getActivityFromTestRule(AndroidComposeTestRule.android.kt:1)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule_androidKt$createAndroidComposeRule$1.invoke(AndroidComposeTestRule.android.kt:116)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule_androidKt$createAndroidComposeRule$1.invoke(AndroidComposeTestRule.android.kt:116)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule$special$$inlined$AndroidComposeUiTestEnvironment$1.getActivity(ComposeUiTest.android.kt:559)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment$AndroidComposeUiTestImpl.getActivity(ComposeUiTest.android.kt:389)
at androidx.compose.ui.test.AndroidComposeUiTestEnvironment$AndroidComposeUiTestImpl.setContent(ComposeUiTest.android.kt:466)
at androidx.compose.ui.test.junit4.AndroidComposeTestRule.setContent(AndroidComposeTestRule.android.kt:340)
at my.test.steps.Steps.I_open_the_App(Steps.kt:41)
at ✽.I open the App(file:/features/app-startup.feature:4)

βœ… What did you expect to see?

Test runs

πŸ“¦ Which tool/library version are you using?

io.cucumber:cucumber-android io.cucumber:cucumber-android-hilt

πŸ”¬ How could we reproduce it?

//Steps.kt
@HiltAndroidTest
class Steps : SemanticsNodeInteractionsProvider {
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
    @get:Rule(order = 1)
    val composeTestRule = createAndroidComposeRule<HiltAppCompatActivity>()

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @Given("I open the App")
    fun I_open_the_App() {
        composeTestRule.setContent {
            Box(Modifier.testTag("Tv").fillMaxSize()) {
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .align(Alignment.Center)
                        .background(Color.Blue)
                )
            }
        }
    }

    @Then("The TV tab is displayed")
    fun Then_the_TV_tab_is_displayed() {
        onNodeWithTag("Tv").assertIsDisplayed()
    }

    override fun onAllNodes(
        matcher: SemanticsMatcher,
        useUnmergedTree: Boolean
    ): SemanticsNodeInteractionCollection = composeTestRule.onAllNodes(matcher, useUnmergedTree)

    override fun onNode(
        matcher: SemanticsMatcher,
        useUnmergedTree: Boolean
    ): SemanticsNodeInteraction = composeTestRule.onNode(matcher, useUnmergedTree)
}

// HiltAppCompatActivity.kt
@AndroidEntryPoint
class HiltAppCompatActivity : AppCompatActivity()

// TestRunner.kt
@CucumberOptions(glue = ["my.test.steps"], features = ["features"])
class TestRunner : CucumberAndroidJUnitRunner() {
    override fun newApplication(cl: ClassLoader, className: String, context: Context): Application =
        super.newApplication(cl, HiltTestApplication::class.java.name, context)
}

Feature file

Feature: App startup

  Scenario: User opens the App
    Given I open the App
    Then The TV tab is displayed

Steps to reproduce the behavior:

  1. Run test
  2. See NullPointerException

Screenshot 2024-01-12 at 09 32 37

πŸ“š Any additional context?

Plain Compose test to compare.

@HiltAndroidTest
class ComposeTest {
    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)
    @get:Rule(order = 1)
    val composeTestRule = createAndroidComposeRule<HiltAppCompatActivity>()

    @Test
    fun I_open_the_App() {
        composeTestRule.setContent {
            Box(Modifier.testTag("Tv").fillMaxSize()) {
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .align(Alignment.Center)
                        .background(Color.Blue)
                )
            }
        }
        composeTestRule.onNodeWithTag("Tv").assertIsDisplayed()
    }
}
lsuski commented 6 months ago

You are missing @WithJunitRule annotation. Also better place rules in separate file. Check readme and cuculator sample

dawidhyzy commented 6 months ago

I checked cukeulator before but it's hard to understand which steps are needed and which are showcase of different approach. I moved the Compose rule to a separate class:

// ComposeRuleHolder.kt
@WithJunitRule
@Singleton
class ComposeRuleHolder @Inject constructor() {
    @get:Rule(order = 1)
    val composeTestRule = createAndroidComposeRule<HiltAppCompatActivity>()
}

And updated the steps accordingly:

@HiltAndroidTest
class Steps(
    val composeRuleHolder: ComposeRuleHolder
) : SemanticsNodeInteractionsProvider by composeRuleHolder.composeTestRule {

    private val composeTestRule
        get() = composeRuleHolder.composeTestRule

    @Given("I open the App")
    fun I_open_the_App() {
        composeTestRule.setContent {
            Box(Modifier.testTag("Tv").fillMaxSize()) {
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .align(Alignment.Center)
                        .background(Color.Blue)
                )
            }
        }
    }

    @Then("The TV tab is displayed")
    fun Then_the_TV_tab_is_displayed() {
        onNodeWithTag("Tv").assertIsDisplayed()
    }
}

I keep getting the same exception. I tried adding @WithJunitRule to both rule holder and steps class but id didn't help.

lsuski commented 6 months ago

I need to check if createAndroidComposeRule could be somehow incompatible. Can you check with createComposeRule. I'm not sure why you need specific activity while you're setting custom composable in test

dawidhyzy commented 6 months ago

I tried createAndroidComposeRule and got the same result.

There are two reasons for using a specific activity

  1. It's a workaround for this issue: https://github.com/google/dagger/issues/3394
  2. I will have in-app language change test cases and for that, I need the host Activity to extend AppCompatActivity https://developer.android.com/guide/topics/resources/app-languages

For backward compatibility with previous Android versions, equivalent APIs are also available in AndroidX. However, the backward compatible APIs work with the AppCompatActivity context

I was checking the pull request and it looks like the cukeulator uses the latest changes that I think are not part of version 7.14.0 could that be a problem?

lsuski commented 6 months ago

Could be, I've tested locally with latest version and createAndroidComposeRule works fine

lsuski commented 6 months ago

But imho the only difference is that @Singleton and @Inject annotation in ComposeRuleHolder is actually ignored in version 7.14.0

dawidhyzy commented 6 months ago

@lsuski I found the problem. I had my steps in my.test.steps and test rules holder in my.test.rule packages. This was causing the NullPointerException despite options pointing to the right package @CucumberOptions(glue = ["my.test.steps"], features = ["features"]). After moving everything to the same package as the test runner and removing glue from the options everything works as expected. Is that a bug or my mistake? If it is a bug I can create a branch which reproduces this behaviour or provide more info.

lsuski commented 6 months ago

it is a mistake, glue points to package where cucumber can find steps, hooks, rules etc.. You can specify more than one package if you want