是否可以使用 Espresso 的 IdlingResource 等待某个视图出现?

Rin*_*dov 11 java android automated-tests android-espresso

在我的测试中,我有一个阶段,在按下按钮后,应用程序会执行大量异步计算并向云服务发出请求,之后它会显示某个视图。

是否可以使用 Espresso 的IdlingResource实现来等待某个视图出现?

我在这里阅读了一个答案,评论似乎表明您可以使用它IdlingResource,但我不明白如何使用。Espresso 似乎没有任何内置的方法来处理长时间的操作,但是必须编写自己的等待循环感觉就像一个黑客。

有什么方法可以解决这个问题,还是我应该按照链接线程中的答案进行操作?

Dmy*_*iev 14

Atte Backenhof 的解决方案有一个小错误(或者我可能不完全理解逻辑)。

getView应该返回 null 而不是抛出异常以使 IdlingResources 工作。

这是带有修复程序的 Kotlin 解决方案:

/**
 * @param viewMatcher The matcher to find the view.
 * @param idleMatcher The matcher condition to be fulfilled to be considered idle.
 */
class ViewIdlingResource(
    private val viewMatcher: Matcher<View?>?,
    private val idleMatcher: Matcher<View?>?
) : IdlingResource {

    private var resourceCallback: IdlingResource.ResourceCallback? = null

    /**
     * {@inheritDoc}
     */
    override fun isIdleNow(): Boolean {
        val view: View? = getView(viewMatcher)
        val isIdle: Boolean = idleMatcher?.matches(view) ?: false
        if (isIdle) {
            resourceCallback?.onTransitionToIdle()
        }
        return isIdle
    }

    /**
     * {@inheritDoc}
     */
    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback?) {
        this.resourceCallback = resourceCallback
    }

    /**
     * {@inheritDoc}
     */
    override fun getName(): String? {
        return "$this ${viewMatcher.toString()}"
    }

    /**
     * Tries to find the view associated with the given [<].
     */
    private fun getView(viewMatcher: Matcher<View?>?): View? {
        return try {
            val viewInteraction = onView(viewMatcher)
            val finderField: Field? = viewInteraction.javaClass.getDeclaredField("viewFinder")
            finderField?.isAccessible = true
            val finder = finderField?.get(viewInteraction) as ViewFinder
            finder.view
        } catch (e: Exception) {
            null
        }
    }

}

/**
 * Waits for a matching View or throws an error if it's taking too long.
 */
fun waitUntilViewIsDisplayed(matcher: Matcher<View?>) {
    val idlingResource: IdlingResource = ViewIdlingResource(matcher, isDisplayed())
    try {
        IdlingRegistry.getInstance().register(idlingResource)
        // First call to onView is to trigger the idler.
        onView(withId(0)).check(doesNotExist())
    } finally {
        IdlingRegistry.getInstance().unregister(idlingResource)
    }
}
Run Code Online (Sandbox Code Playgroud)

在您的 UI 测试中的用法:

    @Test
    fun testUiNavigation() {
        ...
        some initial logic, navigates to a new view
        ...
        waitUntilViewIsDisplayed(withId(R.id.view_to_wait_for))
        ...
        logic on the view that we waited for
        ...
    }
Run Code Online (Sandbox Code Playgroud)

重要更新: IdlingResources 的默认超时为 30 秒,它们不会永远等待。要增加超时,您需要在 @Before 方法中调用它,例如: IdlingPolicies.setIdlingResourceTimeout(3, TimeUnit.MINUTES)

  • 它确实适用于所有情况,您需要增加空闲资源的超时期限。默认值为 30 秒。 (2认同)

Ana*_*lii 12

您的 IdlingResource 可能如下所示:

import android.support.test.espresso.IdlingResource;
import android.support.test.espresso.ViewFinder;
import android.support.test.espresso.ViewInteraction;
import android.view.View;

import org.hamcrest.Matcher;

import java.lang.reflect.Field;

import static android.support.test.espresso.Espresso.onView;

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

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

    private static View getView(Matcher<View> viewMatcher) {
        try {
            ViewInteraction viewInteraction = onView(viewMatcher);
            Field finderField = viewInteraction.getClass().getDeclaredField("viewFinder");
            finderField.setAccessible(true);
            ViewFinder finder = (ViewFinder) finderField.get(viewInteraction);
            return finder.getView();
        } catch (Exception e) {
            return null;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可以创建一个等待您的视图的辅助方法:

public void waitViewShown(Matcher<View> matcher) {
    IdlingResource idlingResource = new ViewShownIdlingResource(matcher);///
    try {
        IdlingRegistry.getInstance().register(idlingResource);
        onView(matcher).check(matches(isDisplayed()));  
    } finally {
        IdlingRegistry.getInstance().unregister(idlingResource);
    }    
}
Run Code Online (Sandbox Code Playgroud)

最后,在您的测试中:

@Test
public void someTest() {
    waitViewShown(withId(R.id.<some>));

    //do whatever verification needed afterwards    
} 
Run Code Online (Sandbox Code Playgroud)

您可以通过让 IdlingResource 等待任何条件(而不仅仅是可见性)来改进此示例。


Att*_*hof 5

我从 Anatolii 中获得了灵感,但我仍然只使用 ViewMatchers,而不是使用 View.class 中的方法。

/**
 * {@link IdlingResource} that idles until a {@link View} condition is fulfilled.
 */
public class ViewIdlingResource implements IdlingResource {

    private final Matcher<View>    viewMatcher;
    private final Matcher<View>    idleMatcher;
    private       ResourceCallback resourceCallback;

    /**
     * Constructor.
     *
     * @param viewMatcher The matcher to find the view.
     * @param idlerMatcher The matcher condition to be fulfilled to be considered idle.
     */
    public ViewIdlingResource(final Matcher<View> viewMatcher, Matcher<View> idlerMatcher) {
        this.viewMatcher = viewMatcher;
        this.idleMatcher = idlerMatcher;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isIdleNow() {
        View view = getView(viewMatcher);
        boolean isIdle = idleMatcher.matches(view);

        if (isIdle && resourceCallback != null) {
            resourceCallback.onTransitionToIdle();
        }

        return isIdle;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        this.resourceCallback = resourceCallback;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getName() {
        return this + viewMatcher.toString();
    }

    /**
     * Tries to find the view associated with the given {@link Matcher<View>}.
     */
    private static View getView(Matcher<View> viewMatcher) {
        try {
            ViewInteraction viewInteraction = onView(viewMatcher);
            Field finderField = viewInteraction.getClass().getDeclaredField("viewFinder");
            finderField.setAccessible(true);
            ViewFinder finder = (ViewFinder) finderField.get(viewInteraction);
            return finder.getView();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以及如何在测试用例中使用闲置器,我将 ViewMatchers.isDisplayed() 作为我在闲置器中的预期条件。

private void waitUntilViewIsDisplayed(Matcher<View> matcher) {
        IdlingResource idlingResource = new ViewIdlingResource(matcher, isDisplayed());
        try {
            IdlingRegistry.getInstance().register(idlingResource);
            // First call to onView is to trigger the idler.
            onView(withId(0)).check(doesNotExist());
        } finally {
            IdlingRegistry.getInstance().unregister(idlingResource);
        }
    }
Run Code Online (Sandbox Code Playgroud)

这样,您可以将任何 Matcher.class 传递给 ViewIdlingResource 构造函数,作为 viewMatcher 参数找到的视图的必需条件。