cucumber / cucumber-android

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

I added steps in another file in the example project, but it failed to run. #42

Closed buptfarmer closed 3 years ago

buptfarmer commented 4 years ago

Firstly, I successfully run the example test project, which passed all the test cases. so I began to make some modification to understand it. I write a new file ChansonSteps.java based on the CalculatorActivitySteps.java, modified steps from "I have a CalculatorActivity" to "Chanson have a CalculatorActivity" and etc. but it fail to run the test cases after the ChansonSteps.java added. Here is the ChansonSteps.java I added

package cucumber.cukeulator.test;

import android.app.Activity;

import androidx.test.rule.ActivityTestRule;

import cucumber.api.java.After;
import cucumber.api.java.Before;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.cukeulator.CalculatorActivity;
import cucumber.cukeulator.R;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.junit.Assert.assertNotNull;

/**
 * We use {@link ActivityTestRule} in order to have access to methods like getActivity
 * and getInstrumentation.
 * </p>
 * The CucumberOptions annotation is mandatory for exactly one of the classes in the test project.
 * Only the first annotated class that is found will be used, others are ignored. If no class is
 * annotated, an exception is thrown.
 * <p/>
 * The options need to at least specify features = "features". Features must be placed inside
 * assets/features/ of the test project (or a subdirectory thereof).
 */

public class ChansonSteps {

    /**
     * Since {@link cucumber.runtime.android.CucumberJUnitRunner} and {@link cucumber.api.android.CucumberInstrumentationCore} have the control over the
     * test lifecycle, activity test rules must not be launched automatically. Automatic launching of test rules is only
     * feasible for JUnit tests. Fortunately, we are able to launch the activity in Cucumber's {@link Before} method.
     */
    ActivityTestRule rule = new ActivityTestRule<>(CalculatorActivity.class, false, false);

    public ChansonSteps(SomeDependency dependency) {
        assertNotNull(dependency);
    }

    /**
     * We launch the activity in Cucumber's {@link Before} hook.
     * See the notes above for the reasons why we are doing this.
     *
     * @throws Exception any possible Exception
     */
    @Before
    public void launchActivity() throws Exception {
        rule.launchActivity(null);
    }

    /**
     * All the clean up of application's data and state after each scenario must happen here
     */
    @After
    public void finishActivity() throws Exception {
        getActivity().finish();
    }

    /**
     * Gets the activity from our test rule.
     *
     * @return the activity
     */
    private Activity getActivity() {
        return rule.getActivity();
    }

    @Given("Chanson have a CalculatorActivity")
    public void Chanson_have_a_CalculatorActivity() {
        assertNotNull(getActivity());
    }

    @When("Chanson press {digit}")
    public void Chanson_press_d(final int d) {
        switch (d) {
            case 0:
                onView(withId(R.id.btn_d_0)).perform(click());
                break;
            case 1:
                onView(withId(R.id.btn_d_1)).perform(click());
                break;
            case 2:
                onView(withId(R.id.btn_d_2)).perform(click());
                break;
            case 3:
                onView(withId(R.id.btn_d_3)).perform(click());
                break;
            case 4:
                onView(withId(R.id.btn_d_4)).perform(click());
                break;
            case 5:
                onView(withId(R.id.btn_d_5)).perform(click());
                break;
            case 6:
                onView(withId(R.id.btn_d_6)).perform(click());
                break;
            case 7:
                onView(withId(R.id.btn_d_7)).perform(click());
                break;
            case 8:
                onView(withId(R.id.btn_d_8)).perform(click());
                break;
            case 9:
                onView(withId(R.id.btn_d_9)).perform(click());
                break;
        }
    }

    @When("Chanson press {operator}")
    public void Chanson_press_op(final char op) {
        switch (op) {
            case '+':
                onView(withId(R.id.btn_op_add)).perform(click());
                break;
            case '–':
                onView(withId(R.id.btn_op_subtract)).perform(click());
                break;
            case 'x':
                onView(withId(R.id.btn_op_multiply)).perform(click());
                break;
            case '/':
                onView(withId(R.id.btn_op_divide)).perform(click());
                break;
            case '=':
                onView(withId(R.id.btn_op_equals)).perform(click());
                break;
        }
    }

    @Then("Chanson should see {string} on the display")
    public void Chanson_should_see_s_on_the_display(final String s) {
        onView(withId(R.id.txt_calc_display)).check(matches(withText(s)));
    }
}

here is the error.

java.lang.NullPointerException: Attempt to invoke virtual method 'void android.app.Activity.finish()' on a null object reference
at cucumber.cukeulator.test.ChansonSteps.finishActivity(ChansonSteps.java:64)
at java.lang.reflect.Method.invoke(Native Method)
at cucumber.runtime.Utils$1.call(Utils.java:26)
at cucumber.runtime.Timeout.timeout(Timeout.java:16)
at cucumber.runtime.Utils.invoke(Utils.java:20)
at cucumber.runtime.java.JavaHookDefinition.execute(JavaHookDefinition.java:60)
at cucumber.runner.HookDefinitionMatch.runStep(HookDefinitionMatch.java:16)
at cucumber.runner.TestStep.executeStep(TestStep.java:65)
at cucumber.runner.TestStep.run(TestStep.java:50)
at cucumber.runner.TestCase.run(TestCase.java:50)
at cucumber.runner.Runner.runPickle(Runner.java:50)
at cucumber.runtime.junit.AndroidPickleRunner.run(AndroidPickleRunner.java:45)
at cucumber.runtime.junit.AndroidFeatureRunner.runChild(AndroidFeatureRunner.java:41)
at cucumber.runtime.junit.AndroidFeatureRunner.runChild(AndroidFeatureRunner.java:12)
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 cucumber.runtime.android.CucumberJUnitRunner.runChild(CucumberJUnitRunner.java:266)
at cucumber.runtime.android.CucumberJUnitRunner.runChild(CucumberJUnitRunner.java:71)
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 cucumber.runtime.android.CucumberJUnitRunner$2.evaluate(CucumberJUnitRunner.java:324)
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:56)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:392)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2119)

so, I want to figure out

  1. what's wrong with my code ? and 2 where is the code to set the steps file CalculatorActivitySteps.java in the CukeulatorAndroidJUnitRunner.java

Thanks for anyone who might help.

lsuski commented 4 years ago

Cucumber @Before and @After works globally in opposite to junit ones which works locally for test class only. It means that before/after each scenario there will be invoked every @Before/@After annotated method in glue package. If you added new annotated methods then it requires different mechanism for managing activity, rule cannot be used. In my company we don't use Activity rule. Instrumentation can be obtained via InstrumentationRegistry and activity can be started with without junit rule by startActivitySync. Each activity started during test can be found inActivityLifecycleRegistry

lsuski commented 4 years ago

According to your 2 question. There is no such code. In cucumber you provide glue package which is used for scanning all classes containing cucumber steps or hook annotations

lsuski commented 4 years ago

For single scenario you can have steps defined in many classes - cucumber instantiate those classes and invokes specific methods. Sample is very simple, maybe to simple. I think that it would be worth to split CalculatorActivitySteps in to 2 classes to show how it works. Activities should not be started in before hook but in concrete step, e.g When I start calculator

lsuski commented 4 years ago

I would also give up finishing activity in after hook because it is naive. Tested activity can start another one which would not be finished then and leaks to next scenario. Start activity with clear top flag which should be sufficient

buptfarmer commented 4 years ago

@lsuski Thanks for your detailed reply. I get it that

  1. I should use only one before/after annotation which is globally applied.
  2. I will try to use another way to manage launching activity.

I am going to make some modification the see the result.

but one question remain

  1. under the glue package, cucumber scan all the defined steps. under the feature asset folder, cucumber scan all the defined features. as there might be quite a few test cases , so if I want to control the the steps or features to be run while I am developing, I should modify the glue package and feature folder. like com.mytest.test.sceneAdd and feature/sceneAdd Am I right?
lsuski commented 4 years ago

You can have many methods annotated with @After/@Before annotation but you have to keep in mind that all will be executed

As cucumber searches for first class annotated with @CucumberOptions in package taken form InstrumentationRegistry.getInstrumentation().getContext().getPackageName() you can have 2 classes in this package - one for local development build (add it to .gitignore) and second one for CI build (in git). I usually name first one a Atest and second one CucumberConfiguration. As first one is for letter 'A' it will be taken by cucumber before this for letter C

dbarwacz commented 4 years ago

I spent some time getting familiar with example as well as cucumber engine and I can agree the fact that all @After/@Before calls will be executed was totally not clear to me. I spent quite a few hours trying to figure out why espresso kept complaining that app is not idle. Only to learn that I was launching two same activities on top of each other, so espresso 'did not see' the second one.

With that said, I was evaluating feasibility of migrating our rather big test infra (~300 tests) to cucumber. I noticed there are some 'rules' (ie constraints) in runner that were not clear to me. I'm mentioning that here, as I believe the fact that all @Befores are run before each scenario is one of them. Can I create a separate issue to mention and hopefully shed some lights on specific use-cases, where current runner is incompatible with our needs? Maybe slack community will be better to have a chat or there are some materials you could point me towards? Thanks!

lsuski commented 4 years ago

@dbarwacz Cucumbr for Android works in most cases the same as cucumber for java so this is rather issue for cucumber-java. @After/@Before should be declared in separate classes dedicated for hooks. For very simple use case it can be declared in same class as steps but I never do that this way

mpkorstanje commented 4 years ago

To clarify; while they look alike; both use annotated methods, both use classes. There is a fundamental difference between JUnit and Cucumber. This difference tends to trip people up the first time round.

Because all classes with annotated methods are part of the test context, it is only logical that all hooks are also executed. They are all in scope.

dbarwacz commented 4 years ago

Thank you for clarifications. This makes total sense now!