在Espresso Android中获取当前活动

56

如果有一个跨越多个活动的测试,有没有一种方法可以获取当前活动?

getActivity() 方法仅提供启动测试时使用的一个活动。

我尝试了下面的代码:

public Activity getCurrentActivity() {
    Activity activity = null;
    ActivityManager am = (ActivityManager) this.getActivity().getSystemService(Context.ACTIVITY_SERVICE);
    List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
    try {
        Class<?> myClass = taskInfo.get(0).topActivity.getClass();
        activity = (Activity) myClass.newInstance();
    }
    catch (Exception e) {

    }
    return activity;
}

但是我得到了一个空对象。


很遗憾,你没有为getActivity()指定类名,否则我会很高兴地获得已启动测试的活动。 - Martin
11个回答

40

在 Espresso 中,您可以使用 ActivityLifecycleMonitorRegistry,但它并没有得到官方支持,因此在将来的版本中可能会失效。

以下是它的工作原理:

Activity getCurrentActivity() throws Throwable {
  getInstrumentation().waitForIdleSync();
  final Activity[] activity = new Activity[1];
  runTestOnUiThread(new Runnable() {
    @Override
    public void run() {
      java.util.Collection<Activity> activities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
      activity[0] = Iterables.getOnlyElement(activities);
  }});
  return activity[0];
}

随着 Espresso 的更新,这可能会变成 null 吗?目前我无法从中获取非 null 值,但以前我是可以的。 - John Shelley
1
使用此代码进行 Espresso 2.0 http://qathread.blogspot.com/2014/09/discovering-espresso-for-android-how-to.html - satyadeepk
4
这里有一份 Kotlin 的类似解决方案:https://dev59.com/W1kT5IYBdhLWcg3wWOJA#58684943 - Oliver Metz
5
这种方法适用于 AndroidX Test 吗? - IgorGanapolsky

30

如果你只需要针对当前的Activity进行检查,可以使用原生的 Espresso 一行代码来检查预期的意图是否已经启动:

intended(hasComponent(new ComponentName(getTargetContext(), ExpectedActivity.class)));

如果您的意图与匹配项不符,Espresso还将显示在此期间触发的意图。

您所需的唯一设置是在测试中将ActivityTestRule替换为IntentsTestRule,以便让它跟踪启动的意图。 并确保此库位于您的build.gradle依赖项中:

androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'

1
我得到了 I/TestRunner: android.support.test.espresso.base.DefaultFailureHandler$AssertionFailedWithCauseError: 想匹配1个意图,实际上匹配了0个意图。 即使当前活动是我断言的相同的。Espresso 2.2.2. @Rule public final ActivityTestRule<MainScreen> main = new ActivityTestRule<>(MainScreen.class); 和断言:intended(hasComponent(new ComponentName(getTargetContext(), MainScreen.class))); - deathangel908
3
这个回答如何回答这个问题? - ericn

15

我喜欢@Ryan的版本,因为它没有使用未记录的内部功能,但你甚至可以把它写得更简短:

private Activity getCurrentActivity() {
    final Activity[] activity = new Activity[1];
    onView(isRoot()).check(new ViewAssertion() {
        @Override
        public void check(View view, NoMatchingViewException noViewFoundException) {
            activity[0] = (Activity) view.getContext();
        }
    });
    return activity[0];
}

请注意,当在 Firebase Test Lab 中运行测试时,此方法无法正常工作。会出现以下错误:

java.lang.ClassCastException: com.android.internal.policy.DecorContext cannot be cast to android.app.Activity

你成功找出 Firebase Test Lab 失败的原因了吗? - Clive Jefferies
7
关于 Firebase 问题,请使用以下代码:activity[0] = (Activity) view.findViewById(android.R.id.content).getContext(); 该代码的作用是将当前视图(view)所在的活动(Activity)上下文(Context)赋值给数组 activity 的第一个元素。 - user2999943
1
你将无法在空闲资源内部使用此功能。check调用实际上会检查是否处于空闲状态:uiController.loopMainThreadUntilIdle(); 因此,您将遇到一个无限循环,其中空闲资源正在等待获取当前活动,直到UI线程处于空闲状态才会返回,而这不会发生,因为您已经注册了空闲资源。我是通过艰难的方式发现这个问题的 =( - tir38
如果你总是只获取一个活动,为什么要创建一个数组? - GabrielBB
1
@GabrielBB 因为在Java中,Lambda表达式中对于外部变量的引用是“有效地final”的,也就是说你不能对这些变量进行赋值。数组是解决这个问题的一种方法。尝试在你喜欢的IDE中将其替换为一个简单的变量,看看错误信息。 - Fabian Streitel
显示剩余4条评论

15
安卓团队已经用ActivityScenario替换了ActivityTestRule。我们可以使用ActivityTestRule进行activityTestRule.getActivity(),但无法使用ActivityScenario。这是我从@Ryan和@Fabian的解决方案中得到的灵感,以下是我的解决方法,用于从ActivityScenario获取一个Activity
@get:Rule
var activityRule = ActivityScenarioRule(MainActivity::class.java)
...
private fun getActivity(): Activity? {
  var activity: Activity? = null
  activityRule.scenario.onActivity {
    activity = it
  }
  return activity
}

2
使用这种方法获取活动时,请小心线程。 - mochadwi
如果我正在测试几个活动的端到端流程,是否有一种使用ActivityScenario获取当前可见活动的方法?我只能获取初始活动。 - David Miguel

9

其他解决方案都没能解决我的问题,所以最终我不得不这样做:

声明你的ActivityTestRule:

@Rule
public ActivityTestRule<MainActivity> mainActivityTestRule =
        new ActivityTestRule<>(MainActivity.class);

声明一个final的Activity数组来存储你的活动:

private final Activity[] currentActivity = new Activity[1];

添加一个帮助方法,以注册应用程序上下文以获取生命周期更新:

private void monitorCurrentActivity() {
    mainActivityTestRule.getActivity().getApplication()
            .registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
                @Override
                public void onActivityCreated(final Activity activity, final Bundle savedInstanceState) { }

                @Override
                public void onActivityStarted(final Activity activity) { }

                @Override
                public void onActivityResumed(final Activity activity) {
                    currentActivity[0] = activity;
                }

                @Override
                public void onActivityPaused(final Activity activity) { }

                @Override
                public void onActivityStopped(final Activity activity) { }

                @Override
                public void onActivitySaveInstanceState(final Activity activity, final Bundle outState) { }

                @Override
                public void onActivityDestroyed(final Activity activity) { }
            });
}

添加一个帮助方法以获取当前活动

private Activity getCurrentActivity() {
    return currentActivity[0];
}

因此,一旦您启动了第一个活动,只需调用monitorCurrentActivity(),然后每当您需要对当前活动的引用时,只需调用getCurrentActivity()


最佳解决方案!谢谢 - Luke Needham

6
public static Activity getActivity() {
    final Activity[] currentActivity = new Activity[1];
    Espresso.onView(AllOf.allOf(ViewMatchers.withId(android.R.id.content), isDisplayed())).perform(new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isAssignableFrom(View.class);
        }

        @Override
        public String getDescription() {
            return "getting text from a TextView";
        }

        @Override
        public void perform(UiController uiController, View view) {
            if (view.getContext() instanceof Activity) {
                Activity activity1 = ((Activity)view.getContext());
                currentActivity[0] = activity1;
            }
        }
    });
    return currentActivity[0];
}

1
一个问题:你为什么要使用数组? - GabrielBB
3
由于当前活动(currentActivity)在匿名函数中被使用,因此必须是final类型的。如果是final类型,那么只能在构造函数中初始化。正因为如此,你要使用一个"Array". - danypata

2

我改进了@Fabian Streitel的答案,这样你就可以在不出现ClassCastException的情况下使用这种方法。

public static Activity getCurrentActivity() {
    final Activity[] activity = new Activity[1];

    onView(isRoot()).check((view, noViewFoundException) -> {

        View checkedView = view;

        while (checkedView instanceof ViewGroup && ((ViewGroup) checkedView).getChildCount() > 0) {

            checkedView = ((ViewGroup) checkedView).getChildAt(0);

            if (checkedView.getContext() instanceof Activity) {
                activity[0] = (Activity) checkedView.getContext();
                return;
            }
        }
    });
    return activity[0];
}

2

基于https://dev59.com/S2Af5IYBdhLWcg3wizWu#50762439,这是一个Kotlin版本的用于访问当前Activity的通用工具类:

class CurrentActivityDelegate(application: Application) {
    private var cachedActivity: Activity? = null

    init {
        monitorCurrentActivity(application)
    }

    fun getCurrentActivity() = cachedActivity

    private fun monitorCurrentActivity(application: Application) {
        application.registerActivityLifecycleCallbacks(
            object : Application.ActivityLifecycleCallbacks {
                override fun onActivityResumed(activity: Activity) {
                    cachedActivity = activity
                    Log.i(TAG, "Current activity updated: ${activity::class.simpleName}")
                }

                override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {}
                override fun onActivityStarted(activity: Activity?) {}
                override fun onActivityPaused(activity: Activity?) {}
                override fun onActivityStopped(activity: Activity?) {}
                override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
                override fun onActivityDestroyed(activity: Activity?) {}
            })
    }
}

然后只需这样使用:

@Before
fun setup() {
    currentActivityDelegate = CurrentActivityDelegate(activityTestRule.activity.application)
}

0
@lacton 提出的解决方案对我没有用,可能是因为活动不处于 ActivityLifecycleMonitorRegistry 报告的状态中。
我甚至尝试了 Stage.PRE_ON_CREATE,仍然没有得到任何活动。 注意:我无法使用 ActivityTestRuleIntentTestRule,因为我是使用 activitiy-alias 启动我的活动,并且在测试中使用实际类没有任何意义,因为我想测试看看别名是否起作用。
我的解决方案是通过 ActivityLifecycleMonitorRegistry 订阅生命周期更改,并阻塞测试线程直到启动活动:
// NOTE: make sure this is a strong reference (move up as a class field) otherwise will be GCed and you will not stably receive updates.
ActivityLifecycleCallback lifeCycleCallback = new ActivityLifecycleCallback() {
            @Override
            public void onActivityLifecycleChanged(Activity activity, Stage stage) {
                classHolder.setValue(((MyActivity) activity).getClass());

                // release the test thread
                lock.countDown();
            }
         };

// used to block the test thread until activity is launched
final CountDownLatch lock = new CountDownLatch(1);
final Holder<Class<? extends MyActivity>> classHolder = new Holder<>();
instrumentation.runOnMainSync(new Runnable() {
   @Override
    public void run() {
        ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(lifeCycleCallback);
     }
});

// start the Activity
intent.setClassName(context, MyApp.class.getPackage().getName() + ".MyActivityAlias");
context.startActivity(intent);
// wait for activity to start
lock.await();

// continue with the tests
assertTrue(classHolder.hasValue());
assertTrue(classHolder.getValue().isAssignableFrom(MyActivity.class));

Holder基本上是一个包装对象。您可以使用数组或其他任何东西来捕获匿名类中的值。


-1

接受的答案在许多 Espresso 测试中可能无法正常工作。以下内容适用于 Espresso 版本 2.2.2 和 Android 编译/目标 SDK 27,在 API 25 设备上运行:

@Nullable
private Activity getActivity() {
    Activity currentActivity = null;

    Collection resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED);
    if (resumedActivities.iterator().hasNext()){
        currentActivity = (Activity) resumedActivities.iterator().next();
    }
    return currentActivity;
}

3
java.lang.IllegalStateException: 在非主线程中查询活动状态是不允许的。 - M. Reza Nasirloo

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