调用getActivity()方法导致RuntimeException异常:无法启动意图Intent act=android.intent.action.MAIN。

13

更新 #1:在本帖末尾添加了更多信息

我对Android开发和测试还不熟悉。

我有三个Espresso测试。第一个测试通过,但第二个测试中setUp()方法中getActivity()的调用总是失败,导致无法运行:

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

第三个测试通过。

创建时我没有任何耗时的操作、动画或网络调用。我可以手动单击应用程序中的所有菜单项并重复测试流程而没有问题。

由于某种原因,在第一个测试之后setUp()中的下一个getActivity()调用会"破坏"第二个测试。在第二个测试之后的所有后续测试都将正常运行。

我发现了类似的问题,但看起来它没有解决,且问题略有不同。

测试代码:

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

}

错误追踪:

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)



更新 #1

我创建了一个干净的eclipse项目和一个干净的Espresso测试用例。我能够在Espresso测试运行时重现相同的错误(但我仍然不确定这个错误的原因是否与我的真实应用程序相同):

目标应用程序有3个活动(主,第二,第三)。用户通过单击菜单项在它们之间导航:主活动启动第二个活动,第二个活动启动第三个。

我发现,在第一个测试中,如果我调用clickMenuItem()(请参见下面的代码)并且Thread.sleep(500),这会导致setUp()中的getActivity()调用崩溃,这发生在第二个测试之前。如果睡眠超时小于0.5秒,则不会崩溃。

同时,如果调用Thread.sleep(10000)而不调用clickMenuItem(),也不会崩溃。它将在第一个测试中成功睡眠10秒,并且还将通过第二个测试。



测试代码:

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());      }    }



目标代码:

主活动:

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

第二个活动:

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

第三项活动:

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;    }
}

我认为你需要粘贴你的活动代码。你的测试看起来很好。 - Steven Mark Ford
1
@StevenMarkFord,感谢您的建议。我在干净的应用程序和测试项目中重现了相同的错误,并在初始问题的末尾发布了源代码(请参见更新#1)。 - bmv2143
如果你完全删除sleep,它会工作吗?为什么你需要在第一次调用时使用sleep呢? - Steven Mark Ford
是的,它可以在没有睡眠调用的情况下工作(也可以在超时小于0.5秒的睡眠情况下工作)。当我试图弄清楚我的生产测试代码出了什么问题时,我在那里放置了睡眠调用。我在我的生产代码中没有睡眠调用,但错误是相同的-并非所有活动都在此测试后关闭。调用sleep方法和clickMenuItem(见上文)会导致这种行为。我找到了一个糟糕的解决方法-在每个测试之后多次按返回按钮以关闭所有打开的活动(请参见下面的答案)。 - bmv2143
4个回答

7

我找到了一种解决方法。我在tearDown()方法中添加了一些代码,多次点击后退按钮来关闭所有之前打开的活动。不幸的是,我没有找到另一种关闭Espresso中所有已打开活动的方法。Robotium有一个非常方便的方法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);
    }
}

2

按"返回"键的解决方案对我无效,但我找到了另一种方法:

@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;
}

2
这对我而言似乎有效,可以关闭堆栈中除第一个活动之外的所有活动。
   @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();

   }

1
尝试以下操作:
@Before
public void setUp() throws Exception {
    super.setUp();
    Log.d(TAG, "SETUP");
    activity = getActivity();
}

1
我尝试过了。不幸的是没有帮助。仍然出现相同的错误。 - bmv2143

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接