如何在没有 IdlingResource 的情况下在 Espresso 中等待异步任务

Posted

技术标签:

【中文标题】如何在没有 IdlingResource 的情况下在 Espresso 中等待异步任务【英文标题】:How to wait for async task in Espresso without IdlingResource 【发布时间】:2020-01-10 21:15:41 【问题描述】:

我有以下浓缩咖啡代码:

@Test
fun backgroundWorkDisplaysTextAfterLoading() 
    onView(withId(R.id.btn_next_fragment)).perform(click())
    onView(withId(R.id.btn_background_work)).perform(click())

    onView(withId(R.id.hiddenTextView)).check(matches(isDisplayed()))

点击btn_background_work时,有一个服务调用会在2秒内返回响应,然后hiddenTextview就会变得可见。

如何让 Espresso 等待执行 ViewAssertion ( check(matches(isDisplayed)) ),直到此服务调用返回一个值?服务调用通过 Retrofit 完成,服务调用在 Schedulers.io() 线程上完成。

我不能接触生产代码,所以在生产代码中实现 IdlingResources 是不可能的。

感谢任何帮助!

【问题讨论】:

【参考方案1】:

您仍然可以在不接触生产代码的情况下实现IdlingResource。你只需要创建一个IdlingResource 回调来监听ViewTreeObserver.OnDrawListener

private class ViewPropertyChangeCallback(private val matcher: Matcher<View>, private val view: View) : IdlingResource, ViewTreeObserver.OnDrawListener 

    private lateinit var callback: IdlingResource.ResourceCallback
    private var matched = false

    override fun getName() = "View property change callback"

    override fun isIdleNow() = matched

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) 
        this.callback = callback
    

    override fun onDraw() 
        matched = matcher.matches(view)
        callback.onTransitionToIdle()
    

然后创建一个自定义的ViewAction 等待匹配:

fun waitUntil(matcher: Matcher<View>): ViewAction = object : ViewAction 

    override fun getConstraints(): Matcher<View> 
        return any(View::class.java)
    

    override fun getDescription(): String 
        return StringDescription().let 
            matcher.describeTo(it)
            "wait until: $it"
        
    

    override fun perform(uiController: UiController, view: View) 
        if (!matcher.matches(view)) 
            ViewPropertyChangeCallback(matcher, view).run 
                try 
                    IdlingRegistry.getInstance().register(this)
                    view.viewTreeObserver.addOnDrawListener(this)
                    uiController.loopMainThreadUntilIdle()
                 finally 
                    view.viewTreeObserver.removeOnDrawListener(this)
                    IdlingRegistry.getInstance().unregister(this)
                
            
        
    

并更新您的测试以使用该操作:

onView(withId(R.id.hiddenTextView)).perform(waitUntil(isDisplayed()))
// or
onView(withId(R.id.hiddenTextView)).perform(waitUntil(withEffectiveVisibility(VISIBLE)))
    .check(matches(isDisplayed()))

没有IdlingResourceThread.sleep(...) 可能是您的下一个选择,但它会不稳定或效率低下。

【讨论】:

【参考方案2】:

使用空闲资源不需要更新生产中的代码。

但这也可以按照您的要求在没有空闲资源的情况下完成,您可以使用此自定义 ViewAction:

/**
 * Perform action of waiting until the element is accessible & not shown.
 * @param viewId The id of the view to wait for.
 * @param millis The timeout of until when to wait for.
 */
public static ViewAction waitUntilShown(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 + "> is shown 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) && child.isShown()) 
                        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();
        
    ;

你可以这样使用它:

onView(isRoot()).perform(waitUntilShown(R.id.hiddenTextView, 5000));

如有必要,更新 5 秒(5000 毫秒)的超时时间。

【讨论】:

以上是关于如何在没有 IdlingResource 的情况下在 Espresso 中等待异步任务的主要内容,如果未能解决你的问题,请参考以下文章

java Android - 如何使用IdlingResource等待JUnit断言的异步操作(可能是Observables或EvenBus)。

java Android - 如何使用IdlingResource等待JUnit断言的异步操作(可能是Observables或EvenBus)。

java Android - 如何使用IdlingResource等待JUnit断言的异步操作(可能是Observables或EvenBus)。

java Android - 如何使用IdlingResource等待JUnit断言的异步操作(可能是Observables或EvenBus)。

java Android - 如何使用IdlingResource等待JUnit断言的异步操作(可能是Observables或EvenBus)。

java Android - 如何使用IdlingResource等待JUnit断言的异步操作(可能是Observables或EvenBus)。