13

Updated #1: more info added to the end of this post

I'm new to Android development and testing.

I have three Espresso tests. First test passes, but the second one will not run because the call to getActivity() in setUp() method before the second test always fails:

Blockquote java.lang.RuntimeException: Could not launch intent Intent { act=android.intent.action.MAIN flg=0x10000000 cmp=my.packagename/.ActivityMain } within 45 seconds. ...

The third test passes.

I don't have any long running operations, animations or network calls on creation. I can click through all menu items in my app manually repeating the test flow without issues.

For some reason the first test "breaks" next getActivity() call in setUp() before the second test. All following tests (after the second test) will run OK.

I found similar question but it looks like it wasn't resolved and it has a little bit different problem.

Test code:

import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import net.humblegames.bodylasticscalculator.ActivityMain;
import net.humblegames.bodylasticscalculator.R;
import net.humblegames.bodylasticscalculator.applogic.BandSystem;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Before;

import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;

public class MenuNavigationTestExperiment extends
        ActivityInstrumentationTestCase2<ActivityMain> {

    private String TAG = getClass().getSimpleName();
    private Activity activity;

    public MenuNavigationTestExperiment() {
        super(ActivityMain.class);
    }


    @Before
    public void setUp() throws Exception {
        Log.d(TAG, "SETUP");
        activity = getActivity();

        super.setUp();
    }

    @After
    public void tearDown() throws Exception {
        Log.d(TAG, "TEARDOWN");

        super.tearDown();
    }


    public void testFirst() {
        Log.d(TAG, "testFirst");

        clickMenuItem("Select band system");
        onData(allOf(is(instanceOf(BandSystem.class)), hasName("MMA Training")))
        .onChildView(withId(R.id.label)).perform(click());

        clickMenuItem("About");
        onView(withId(R.id.about_webview)).check(matches(isDisplayed()));
    }

    public void testSecond() {
        Log.d(TAG, "testSecond");
        // this test will not run
    }


    public void testThird() {
        Log.d(TAG, "testThird");
        // this test will pass
    }

    // ------------------ HELPER METHODS ---------------------------------------

    private void clickMenuItem(String menuItem) {
        Log.d(TAG, "clickMenuItem");

        openActionBarOverflowOrOptionsMenu(getInstrumentation()
                .getTargetContext());

        onView(withText(menuItem)).perform(click());
    }

    private Matcher<BandSystem> hasName(final String name) {
        Log.d(TAG, "hasName");

        return new BaseMatcher<BandSystem>() {
            @Override
            public boolean matches(final Object item) {
                final BandSystem foo = (BandSystem) item;
                return name == foo.getName();
            }

            @Override
            public void describeTo(final Description description) {
                description.appendText("getName should return ").appendValue(
                        name);
            }
        };
    }

}

Error trace:

java.lang.RuntimeException: Could not launch intent Intent { act=android.intent.action.MAIN flg=0x10000000 cmp=net.humblegames.bodylasticscalculator/.ActivityMain } within 45 seconds. Perhaps the main thread has not gone idle within a reasonable amount of time? There could be an animation or something constantly repainting the screen. Or the activity is doing network calls on creation? See the threaddump logs. For your reference the last time the event queue was idle before your activity launch request was 1395582828351 and and now the last time the queue went idle was: 1395582830169. If these numbers are the same your activity might be hogging the event queue.
at com.google.android.apps.common.testing.testrunner.GoogleInstrumentation.startActivitySync(GoogleInstrumentation.java:277)
at android.test.InstrumentationTestCase.launchActivityWithIntent(InstrumentationTestCase.java:119)
at android.test.InstrumentationTestCase.launchActivity(InstrumentationTestCase.java:97)
at android.test.ActivityInstrumentationTestCase2.getActivity(ActivityInstrumentationTestCase2.java:104)
at net.humblegames.bodylasticscalculator.test.MenuNavigationTestExperiment.setUp(MenuNavigationTestExperiment.java:42)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:191)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:176)
at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:554)
at com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner.onStart(GoogleInstrumentationTestRunner.java:167)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1701)



Update #1

I created a clean eclipse project and a clean Espresso test for that project. I was able to reproduce the same error during Espresso test run (but I'm still unsure if the cause of this error is the same in my real app):

Target app has 3 activities (main, second, third). User navigates between them by clicking menu items: main activity starts the second, the second activity starts the third.

I found that if I call clickMenuItem() (see below) AND Thread.sleep(500) in the first test this causes crash in setUp() in getActivity() call before the second test. If timeout in sleep is less than 0.5 sec there is no crash.

At the same time if call Thread.sleep(10000) and don't call clickMenuItem() there is no crash also. It will successfully sleep for 10 seconds in the first test and will also pass the second one.



Test code:
public class MainActivityTest extends
        ActivityInstrumentationTestCase2<MainActivity> {

    private String TAG = getClass().getSimpleName();
    private Activity activity;

    public MainActivityTest() {
        super(MainActivity.class);    }

    public void setUp() throws Exception {
        Log.d(TAG, "SETUP");
        activity = getActivity();

        super.setUp();       }

    public void tearDown() throws Exception {
        Log.d(TAG, "TEARDOWN");

        super.tearDown();    }


    public void testFirst() throws InterruptedException {
        Log.d(TAG, "testFirst");

        clickMenuItem("Start second activity");

        clickMenuItem("Start third activity");

        Thread.sleep(500);
    }

    public void testSecond() {
        Log.d(TAG, "testSecond");
        // this test will not run     }


    private void clickMenuItem(String menuItem) {
        Log.d(TAG, "clickMenuItem");

        openActionBarOverflowOrOptionsMenu(getInstrumentation()
                .getTargetContext());

        onView(withText(menuItem)).perform(click());      }    }



Target code:

Main Activity:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);         }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;        }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
        case R.id.action_start_second_activity:
            Intent intent = new Intent(this, SecondActivity.class);
            startActivity(intent);
            return true;

        default:
            return super.onOptionsItemSelected(item);
        }       }        }

Second Activity:

public class SecondActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);       }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.second, menu);
        return true;        }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle item selection
        switch (item.getItemId()) {
        case R.id.action_start_third_activity:
            Intent intent = new Intent(this, ThirdActivity.class);
            startActivity(intent);
            return true;

        default:
            return super.onOptionsItemSelected(item);
        }       }   }

Third Activity:

public class ThirdActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.third, menu);
        return true;    }
}
Community
  • 1
  • 1
bmv2143
  • 734
  • 1
  • 8
  • 17
  • I think you have to paste your activity code. Your tests look fine. – Steven Mark Ford Mar 24 '14 at 20:02
  • 1
    @StevenMarkFord , thanks for the suggestion. I reproduced the same error in clean app and test projects and posted the source at the end of the initial question (see Update #1). – bmv2143 Mar 25 '14 at 20:57
  • If you remove the sleep completely does it work? Why do you need to call sleep in the first place? – Steven Mark Ford Mar 27 '14 at 10:02
  • Yes, it works without sleep call (also it works with sleep that has timeouts < 0.5 sec). I put sleep call there when I was trying to figure out what was wrong with my production test code. I don't have sleep call in my production code, but the error is the same - not all activities are closed after this test. Calling sleep method AND clickMenuItem (see above) causes such behaviour. I found a bad workaround - pressing back button multiple times after each test to close all opened activities (see my answer below). – bmv2143 Mar 27 '14 at 15:40

4 Answers4

7

I found a workaround. I added code that hits back button multiple times in the tearDown() method to close all previously opened activities. Unfortunately I didn't found another method how to close all opened activities in Espresso. Robotium has a very convenient method for that purpose solo.finishOpenedActivities().

public void tearDown() throws Exception {
    Log.d(TAG, "TEARDOWN");

    goBackN();

    super.tearDown();
}

private void goBackN() {
    final int N = 10; // how many times to hit back button
    try {
        for (int i = 0; i < N; i++)
            Espresso.pressBack();
    } catch (com.google.android.apps.common.testing.ui.espresso.NoActivityResumedException e) {
        Log.e(TAG, "Closed all activities", e);
    }
}
bmv2143
  • 734
  • 1
  • 8
  • 17
2

pressBack solution does not worked for me but I found another:

@Override
protected void tearDown() throws Exception {
    closeAllActivities(getInstrumentation());
    super.tearDown();
}

public static void closeAllActivities(Instrumentation instrumentation) throws Exception {
    final int NUMBER_OF_RETRIES = 100;
    int i = 0;
    while (closeActivity(instrumentation)) {
        if (i++ > NUMBER_OF_RETRIES) {
            throw new AssertionError("Limit of retries excesses");
        }
        Thread.sleep(200);
    }
}

public static <X> X callOnMainSync(Instrumentation instrumentation, final Callable<X> callable) throws Exception {
    final AtomicReference<X> retAtomic = new AtomicReference<>();
    final AtomicReference<Throwable> exceptionAtomic = new AtomicReference<>();
    instrumentation.runOnMainSync(new Runnable() {
        @Override
        public void run() {
            try {
                retAtomic.set(callable.call());
            } catch (Throwable e) {
                exceptionAtomic.set(e);
            }
        }
    });
    final Throwable exception = exceptionAtomic.get();
    if (exception != null) {
        Throwables.propagateIfInstanceOf(exception, Exception.class);
        Throwables.propagate(exception);
    }
    return retAtomic.get();
}

public static Set<Activity> getActivitiesInStages(Stage... stages) {
    final Set<Activity> activities = Sets.newHashSet();
    final ActivityLifecycleMonitor instance = ActivityLifecycleMonitorRegistry.getInstance();
    for (Stage stage : stages) {
        final Collection<Activity> activitiesInStage = instance.getActivitiesInStage(stage);
        if (activitiesInStage != null) {
            activities.addAll(activitiesInStage);
        }
    }
    return activities;
}

private static boolean closeActivity(Instrumentation instrumentation) throws Exception {
    final Boolean activityClosed = callOnMainSync(instrumentation, new Callable<Boolean>() {
        @Override
        public Boolean call() throws Exception {
            final Set<Activity> activities = getActivitiesInStages(Stage.RESUMED,
                    Stage.STARTED, Stage.PAUSED, Stage.STOPPED, Stage.CREATED);
            activities.removeAll(getActivitiesInStages(Stage.DESTROYED));
            if (activities.size() > 0) {
                final Activity activity = activities.iterator().next();
                activity.finish();
                return true;
            } else {
                return false;
            }
        }
    });
    if (activityClosed) {
        instrumentation.waitForIdleSync();
    }
    return activityClosed;
}
Testing Singh
  • 1,347
  • 1
  • 15
  • 26
2

This seems to work for me to close all of the activities in the stack except the first one.

   @After
   @Override
   public void tearDown() throws Exception
   {
      Intent intent = new Intent(getActivity(), getActivity().getClass());
      intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // Removes other Activities from stack
      intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
      myActivity.startActivity(intent);
      super.tearDown();

   }
harmanjd
  • 1,874
  • 19
  • 21
1

Try the following:

@Before
public void setUp() throws Exception {
    super.setUp();
    Log.d(TAG, "SETUP");
    activity = getActivity();
}
Shereef Marzouk
  • 3,282
  • 7
  • 41
  • 64