在使用 Espresso 进行单元测试的时候,我们需要等待被测试界面上某个元素显示出来,这个时候需要进行等待,等待的时间需要我们自行控制。
参考代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/** Perform action of waiting for a specific view id. */ public static ViewAction waitId(final int viewId, final long millis) { return new ViewAction() { @Override public Matcher<View> getConstraints() { return isRoot(); } @Override public String getDescription() { return "wait for a specific view with id <" + viewId + "> during " + millis + " millis."; } @Override public void perform(final UiController uiController, final View view) { uiController.loopMainThreadUntilIdle(); final long startTime = System.currentTimeMillis(); final long endTime = startTime + millis; final Matcher<View> viewMatcher = withId(viewId); do { for (View child : TreeIterables.breadthFirstViewTraversal(view)) { // found view with required ID if (viewMatcher.matches(child)) { return; } } uiController.loopMainThreadForAtLeast(50); } while (System.currentTimeMillis() < endTime); // timeout happens throw new PerformException.Builder() .withActionDescription(this.getDescription()) .withViewDescription(HumanReadables.describe(view)) .withCause(new TimeoutException()) .build(); } }; } |
使用方式如下:
1 2 |
// wait during 15 seconds for a view onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15))); |
不过,更推荐使用 IdlingResource 实现上述的功能,IdlingResource 的 适用范围更广,不仅可以实现 UI 的等待,也可以实现网络,异步调用返回,比如:Handler.postDelayed 等情况。
例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import static androidx.test.espresso.Espresso.onView; import android.view.View; import androidx.annotation.Nullable; import androidx.test.espresso.IdlingResource; import androidx.test.espresso.ViewFinder; import androidx.test.espresso.ViewInteraction; import org.hamcrest.Matcher; import java.lang.reflect.Field; public class ViewShownIdlingResource implements IdlingResource { private static final String TAG = ViewShownIdlingResource.class.getSimpleName(); private final Matcher<View> viewMatcher; private ResourceCallback resourceCallback; public ViewShownIdlingResource(final Matcher<View> viewMatcher) { this.viewMatcher = viewMatcher; } @Nullable private static View getView(Matcher<View> viewMatcher) { try { final ViewInteraction viewInteraction = onView(viewMatcher); final Field finderField = viewInteraction.getClass().getDeclaredField("viewFinder"); finderField.setAccessible(true); final ViewFinder finder = (ViewFinder) finderField.get(viewInteraction); assert finder != null; return finder.getView(); } catch (Exception e) { return null; } } @Override public boolean isIdleNow() { View view = getView(viewMatcher); boolean idle = view == null || view.isShown(); if (idle && resourceCallback != null) { resourceCallback.onTransitionToIdle(); } return idle; } @Override public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { this.resourceCallback = resourceCallback; } @Override public String getName() { return this + viewMatcher.toString(); } } |
使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public void waitViewShown(Matcher<View> matcher) { final IdlingResource idlingResource = new ViewShownIdlingResource(matcher);/// final ActivityScenario<MainActivity> activityScenario = ActivityScenario.launch(MainActivity.class); activityScenario.onActivity(new ActivityScenario.ActivityAction<MainActivity>() { @Override public void perform(MainActivity activity) { // 注册,注销,需要从主线程调用 IdlingRegistry.getInstance().register(idlingResource); } }); try { onView(matcher).check(matches(isDisplayed())); } finally { activityScenario.onActivity(new ActivityScenario.ActivityAction<MainActivity>() { @Override public void perform(MainActivity activity) { // 注册,注销,需要从主线程调用 IdlingRegistry.getInstance().unregister(idlingResource); } }); } } |
测试用例使用:
1 2 3 4 5 6 |
@Test public void someTest() { waitViewShown(withId(R.id.<some>)); //do whatever verification needed afterwards } |
在使用 IdlingResource 进行单元测试的时候,需要注意:IdlingResource 只有被调用
1 |
InstrumentationRegistry.getInstrumentation().waitForIdleSync(); |
的时候,才能实现等待异步资源调用完成。Espresso 的 API 内部会自动调用。但是我们自己写测试用例的时候,很多时候需要手工调用这个 API 实现同步等待。如果使用 IdlingResource 的时候,结果与预期存在差异,可以尝试手工增加一下这个 API 的调用。
比如类似如下的场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Test public void waitIdlingResource() { final CountingIdlingResource idlingResource = new CountingIdlingResource("async");/// final ActivityScenario<MainActivity> activityScenario = ActivityScenario.launch(MainActivity.class); activityScenario.onActivity(new ActivityScenario.ActivityAction<MainActivity>() { @Override public void perform(MainActivity activity) { // 注册,注销,需要从主线程调用 IdlingRegistry.getInstance().register(idlingResource); } }); try { final AtomicBoolean val = new AtomicBoolean(false); /// 做一些操作,比如异步操作完成后修改val,并且把 idlingResource 设置为空闲,但是不涉及UI操作或者 Espresso API的调用 // 等待idlingResource变成完成状态 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); Assert.assertTrue(val.get()); } finally { activityScenario.onActivity(new ActivityScenario.ActivityAction<MainActivity>() { @Override public void perform(MainActivity activity) { // 注册,注销,需要从主线程调用 IdlingRegistry.getInstance().unregister(idlingResource); } }); } } |
官方提供了一个例子: IdlingResourceSample:与后台作业同步
如果存在访问问题,可以 点击此处下载一份代码的镜像。
另外,针对 Robolectric 进行 UI 测试的情况,如果需要等待 Handler.postDelayed 的事件完成,可以通过
1 |
ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); |
来实现等待异步事件执行完成。
有时候,我们只是想简单的等待几秒时间,这几秒时间我们希望不影响被测试线程的执行。比如启动一个 Activity 然后等待页面初始化完成,网上看到的一种写法如下:
1 2 3 |
public static void waitForIdle(final long millis) { Espresso.onView(ViewMatchers.isRoot()).perform(waitFor(millis)); } |
上述的等待方法在大部分情况下是可以正常运行的。
但是,也经常报错如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
java.lang.RuntimeException: Waited for the root of the view hierarchy to have window focus and not request layout for 10 seconds. If you specified a non default root matcher, it may be picking a root that never takes focus. Root: Root{application-window-token=android.view.ViewRootImpl$W@e1847f, window-token=android.view.ViewRootImpl$W@e1847f, has-window-focus=false, layout-params-type=1, layout-params-string={(0,0)(fillxfill) ty=BASE_APPLICATION wanim=0x10302fe fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS pfl=FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED fitSides=}, decor-view-string=DecorView{id=-1, visibility=VISIBLE, width=480, height=854, has-focus=false, has-focusable=true, has-window-focus=false, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params={(0,0)(fillxfill) ty=BASE_APPLICATION wanim=0x10302fe fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS pfl=FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED fitSides=}, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}} at androidx.test.espresso.base.RootViewPicker.waitForRootToBeReady(RootViewPicker.java:69) at androidx.test.espresso.base.RootViewPicker.pickRootView(RootViewPicker.java:37) at androidx.test.espresso.base.RootViewPicker.get(RootViewPicker.java:16) at androidx.test.espresso.ViewInteractionModule.provideRootView(ViewInteractionModule.java:9) at androidx.test.espresso.ViewInteractionModule_ProvideRootViewFactory.provideRootView(ViewInteractionModule_ProvideRootViewFactory.java:8) at androidx.test.espresso.ViewInteractionModule_ProvideRootViewFactory.get(ViewInteractionModule_ProvideRootViewFactory.java:6) at androidx.test.espresso.ViewInteractionModule_ProvideRootViewFactory.get(ViewInteractionModule_ProvideRootViewFactory.java:7) at androidx.test.espresso.base.ViewFinderImpl.getView(ViewFinderImpl.java:12) at androidx.test.espresso.ViewInteraction.doPerform(ViewInteraction.java:48) at androidx.test.espresso.ViewInteraction.access$100(ViewInteraction.java:15) at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:3) at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:2) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:223) at android.app.ActivityThread.main(ActivityThread.java:7656) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) |
上述报错的原因是:由于 Activity 的启动是异步的,ViewMatchers 在尝试获取根 View 的时候,默认是获取当前顶部的 Activity ,此时可能会出现获取的 Activity 不正确,是一个即将切换到后台的 Activity 的问题,一旦这个问题发生,会出现上述的报错,导致测试失败。更详细解释,可以参考 Multi-window sample for Espresso 。
其实上述的代码可以用下面的代码替换:
1 2 3 4 5 |
public static void waitForIdle(final long millis) { try { InstrumentationRegistry.getInstrumentation().getUiAutomation().waitForIdle(millis, millis); } catch(TimeoutException ignored) {} } |
类似的常用的等待函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static boolean waitForIdle(@Nullable final Callable<Boolean> callable, final long millis) throw Exception { final long sartTime = SystemClock.elapsedRealtime(); final long endTime = sartTime + millis; do { if(null != callable) { if(callable.call() { return true; } } InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } while(SystemClock.elapsedRealtime() < endTime); return false; } |
另外目前遇到的问题还有,在 Android Studio Electric El|2022.1.1 Patch2 上点击 Debug 可以正常运行测试用例,但是点击 Run 就会测试失败,报错如下:
1 2 3 4 5 6 7 |
W/ActivityManager: Crash of app com.example.testsample running instrumentation ComponentInfo{com.example.testsample.test/android.support.test.runner.AndroidJUnitRunner} 07-16 19:19:34.191 7834-7850/? W/Binder: Binder call failed. java.lang.SecurityException: Calling from not trusted UID! at android.app.UiAutomationConnection.throwIfCalledByNotTrustedUidLocked(UiAutomationConnection.java:427) at android.app.UiAutomationConnection.shutdown(UiAutomationConnection.java:324) at android.app.IUiAutomationConnection$Stub.onTransact(IUiAutomationConnection.java:209) at android.os.Binder.execTransact(Binder.java:570) |
貌似重启手机、重启Android Studio Electric El|2022.1.1 Patch2 、项目重新构建一次、用老版本的 Android Studio 4.1.3 打开一下项目然后关闭能解决,具体是哪个操作解决的暂时不清楚。
参考链接
- Espresso: Thread.sleep( )
- ESPRESSO: WAIT FOR ELEMENT
- Android Testing Handler.postDelayed
- Espresso 空闲资源
- Is it possible to use Espresso's IdlingResource to wait until a certain view appears?
- Espresso Tests fail on Android 11 #751
- Multi-window sample for Espresso
- RunTimeException in Android espresso when selecting spinner in dialog
- Calling from not trusted UID