是否可以使用 Espresso 的 IdlingResource 等待某个视图出现?
Is it possible to use Espresso's IdlingResource to wait until a certain view appears?
在我的测试中,我有一个阶段,在按下按钮后,应用程序会执行大量异步计算并向云服务发出请求,之后它会显示特定视图。
是否可以使用 Espresso 的 IdlingResource
实现来等待某个视图出现?
我读过一个答案 here,评论似乎建议您可以改用 IdlingResource
,但我不明白该怎么做。 Espresso 似乎没有任何内置的方法来处理长时间的操作,但不得不编写自己的等待循环感觉就像一个 hack。
有什么方法可以解决这个问题,还是我应该按照链接线程中的答案建议的那样做?
您的 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;
}
}
}
然后,您可以创建一个等待您查看的辅助方法:
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);
}
}
最后,在你的测试中:
@Test
public void someTest() {
waitViewShown(withId(R.id.<some>));
//do whatever verification needed afterwards
}
您可以通过使 IdlingResource 等待任何条件,而不仅仅是为了可见性条件来改进此示例。
我从 Anatolii 那里获得灵感,但我没有使用 View.class 中的方法,我仍然只使用 ViewMatchers。
/**
* {@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);
}
}
}
以及如何在您的测试用例中使用惰轮,我将 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);
}
}
有了这个,您可以将任何 Matcher.class 传递给 ViewIdlingResource 构造函数,作为 viewMatcher 参数找到的视图的必需条件。
Atte Backenhof 的解决方案有一个小错误(或者我没有完全理解其中的逻辑)。
getView 应该 return 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)
}
}
在您的 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
...
}
重要更新: IdlingResources 的默认超时为 30 秒,它们不会永远等待。要增加超时,您需要在 @Before 方法中调用它,例如:
IdlingPolicies.setIdlingResourceTimeout(3, TimeUnit.MINUTES)
等待视图显示并可以对其执行操作。你可以使用这个方法:
private const val sleepTime = 1000L
private const val maximumWaitedTime = 10000L // maximum waited time in milliseconds to wait a view visible
fun waitViewVisible(viewInteraction: ViewInteraction?, block: (() -> Unit)? = null) {
waitAssertView(viewInteraction, ViewAssertions.matches(isDisplayed()), block)
}
fun waitViewGone(viewInteraction: ViewInteraction?, block: (() -> Unit)? = null) {
waitAssertView(viewInteraction, ViewAssertions.matches(not(isDisplayed())), block)
}
fun waitAssertView(viewInteraction: ViewInteraction?, assertion: ViewAssertion?, block: (() -> Unit)? = null) {
if (viewInteraction == null || assertion == null) throw NullPointerException()
val startedTime: Long = System.currentTimeMillis()
var elapsedTime: Long = 0
var isVisible = false
do {
isVisible = runCatching {
viewInteraction.check(assertion)
}.isSuccess
if (isVisible) break
Thread.sleep(sleepTime)
elapsedTime = System.currentTimeMillis() - startedTime
} while (elapsedTime <= maximumWaitedTime)
if (!isVisible) throw TimeoutException("Waited time exceed the maximum waited time")
block?.invoke()
}
在我的测试中,我有一个阶段,在按下按钮后,应用程序会执行大量异步计算并向云服务发出请求,之后它会显示特定视图。
是否可以使用 Espresso 的 IdlingResource
实现来等待某个视图出现?
我读过一个答案 here,评论似乎建议您可以改用 IdlingResource
,但我不明白该怎么做。 Espresso 似乎没有任何内置的方法来处理长时间的操作,但不得不编写自己的等待循环感觉就像一个 hack。
有什么方法可以解决这个问题,还是我应该按照链接线程中的答案建议的那样做?
您的 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;
}
}
}
然后,您可以创建一个等待您查看的辅助方法:
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);
}
}
最后,在你的测试中:
@Test
public void someTest() {
waitViewShown(withId(R.id.<some>));
//do whatever verification needed afterwards
}
您可以通过使 IdlingResource 等待任何条件,而不仅仅是为了可见性条件来改进此示例。
我从 Anatolii 那里获得灵感,但我没有使用 View.class 中的方法,我仍然只使用 ViewMatchers。
/**
* {@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);
}
}
}
以及如何在您的测试用例中使用惰轮,我将 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);
}
}
有了这个,您可以将任何 Matcher.class 传递给 ViewIdlingResource 构造函数,作为 viewMatcher 参数找到的视图的必需条件。
Atte Backenhof 的解决方案有一个小错误(或者我没有完全理解其中的逻辑)。
getView 应该 return 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)
}
}
在您的 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
...
}
重要更新: IdlingResources 的默认超时为 30 秒,它们不会永远等待。要增加超时,您需要在 @Before 方法中调用它,例如:
IdlingPolicies.setIdlingResourceTimeout(3, TimeUnit.MINUTES)
等待视图显示并可以对其执行操作。你可以使用这个方法:
private const val sleepTime = 1000L
private const val maximumWaitedTime = 10000L // maximum waited time in milliseconds to wait a view visible
fun waitViewVisible(viewInteraction: ViewInteraction?, block: (() -> Unit)? = null) {
waitAssertView(viewInteraction, ViewAssertions.matches(isDisplayed()), block)
}
fun waitViewGone(viewInteraction: ViewInteraction?, block: (() -> Unit)? = null) {
waitAssertView(viewInteraction, ViewAssertions.matches(not(isDisplayed())), block)
}
fun waitAssertView(viewInteraction: ViewInteraction?, assertion: ViewAssertion?, block: (() -> Unit)? = null) {
if (viewInteraction == null || assertion == null) throw NullPointerException()
val startedTime: Long = System.currentTimeMillis()
var elapsedTime: Long = 0
var isVisible = false
do {
isVisible = runCatching {
viewInteraction.check(assertion)
}.isSuccess
if (isVisible) break
Thread.sleep(sleepTime)
elapsedTime = System.currentTimeMillis() - startedTime
} while (elapsedTime <= maximumWaitedTime)
if (!isVisible) throw TimeoutException("Waited time exceed the maximum waited time")
block?.invoke()
}