Espresso: Thread.sleep()

Espresso 声称不需要 Thread.sleep(),但是我的代码不工作,除非我包含它。我正在连接到一个 IP,当连接时,显示一个进度对话框。我需要一个 Thread.sleep()调用,以等待对话解散。下面是我使用它的测试代码:

    IP.enterIP(); // fills out an IP dialog (this is done with espresso)


//progress dialog is now shown
Thread.sleep(1500);


onView(withId(R.id.button).perform(click());

我已经尝试了这个代码没有 Thread.sleep()调用,但它说 R.id.Button不存在。我唯一能让它工作的方法就是打 Thread.sleep()电话。

另外,我试过用像 getInstrumentation().waitForIdleSync()这样的东西来替换 Thread.sleep(),但仍然没有结果。

这是唯一的办法吗? 还是我遗漏了什么?

先谢谢你。

82055 次浏览

Espresso 的构建是为了避免测试中的 sleep ()调用。您的测试不应该打开一个输入 IP 的对话框,这应该是测试活动的责任。

另一方面,你的 UI 测试应该:

  • 等待 IP 对话框出现
  • 填写 IP 地址并单击回车
  • 等待按钮出现并单击它

测试应该是这样的:

// type the IP and press OK
onView (withId (R.id.dialog_ip_edit_text))
.check (matches(isDisplayed()))
.perform (typeText("IP-TO-BE-TYPED"));


onView (withText (R.string.dialog_ok_button_title))
.check (matches(isDisplayed()))
.perform (click());


// now, wait for the button and click it
onView (withId (R.id.button))
.check (matches(isDisplayed()))
.perform (click());

在执行测试之前,Espresso 会等待 UI 线程和 AsyncTask 池中发生的所有事情都完成。

请记住,您的测试不应该做任何属于您的应用程序责任的事情。它的行为应该像一个“消息灵通的用户”: 一个用户点击,验证屏幕上显示了什么,但事实上,知道组件的 ID

在我看来,正确的做法是:

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

然后使用模式将是:

// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));

我在寻找类似问题的答案时偶然发现了这个线程,当时我正在等待服务器响应并根据响应改变元素的可见性。

虽然上面的解决方案肯定有所帮助,但我最终还是找到了 this excellent example from chiuki,现在只要我在应用程序空闲期间等待动作发生,就可以使用这种方法。

我已经将 资源()添加到我自己的实用程序类中,现在可以有效地使用它作为特浓咖啡的替代品,而且现在的用法是干净利落的:

// Make sure Espresso does not time out
IdlingPolicies.setMasterPolicyTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);
IdlingPolicies.setIdlingResourceTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);


// Now we wait
IdlingResource idlingResource = new ElapsedTimeIdlingResource(waitingTime);
Espresso.registerIdlingResources(idlingResource);


// Stop and verify
onView(withId(R.id.toggle_button))
.check(matches(withText(R.string.stop)))
.perform(click());
onView(withId(R.id.result))
.check(matches(withText(success ? R.string.success: R.string.failure)));


// Clean up
Espresso.unregisterIdlingResources(idlingResource);

感谢亚历克斯的回答。有些情况下,您需要在代码中进行一些延迟。它不一定等待服务器响应,但可能等待动画完成。我个人对 Espresso idolingResources 有疑问(我认为我们正在为一件简单的事情编写许多行代码) ,因此我将 AlexK 的方式改为以下代码:

/**
* Perform action of waiting for a specific time.
*/
public static ViewAction waitFor(final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}


@Override
public String getDescription() {
return "Wait for " + millis + " milliseconds.";
}


@Override
public void perform(UiController uiController, final View view) {
uiController.loopMainThreadForAtLeast(millis);
}
};
}

因此,您可以创建一个 Delay类,并将此方法放入其中,以便轻松地访问它。 您可以在 Test 类中以相同的方式使用它: onView(isRoot()).perform(waitFor(5000));

我认为加上这句话更容易:

SystemClock.sleep(1500);

在返回之前等待给定的毫秒数(uptimeMillis)。与 sleep (long)类似,但不抛出 InterruptedException; 中断()事件被推迟到下一个可中断操作。直到至少经过指定的毫秒数才返回。

虽然我认为最好使用空闲资源(https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/)来完成这个任务,但是你也许可以把它作为一个备用方案:

/**
* Contains view interactions, view actions and view assertions which allow to set a timeout
* for finding a view and performing an action/view assertion on it.
* To be used instead of {@link Espresso}'s methods.
*
* @author Piotr Zawadzki
*/
public class TimeoutEspresso {


private static final int SLEEP_IN_A_LOOP_TIME = 50;


private static final long DEFAULT_TIMEOUT_IN_MILLIS = 10 * 1000L;


/**
* Use instead of {@link Espresso#onView(Matcher)}
* @param timeoutInMillis timeout after which an error is thrown
* @param viewMatcher view matcher to check for view
* @return view interaction
*/
public static TimedViewInteraction onViewWithTimeout(long timeoutInMillis, @NonNull final Matcher<View> viewMatcher) {


final long startTime = System.currentTimeMillis();
final long endTime = startTime + timeoutInMillis;


do {
try {
return new TimedViewInteraction(Espresso.onView(viewMatcher));
} catch (NoMatchingViewException ex) {
//ignore
}


SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
}
while (System.currentTimeMillis() < endTime);


// timeout happens
throw new PerformException.Builder()
.withCause(new TimeoutException("Timeout occurred when trying to find: " + viewMatcher.toString()))
.build();
}


/**
* Use instead of {@link Espresso#onView(Matcher)}.
* Same as {@link #onViewWithTimeout(long, Matcher)} but with the default timeout {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
* @param viewMatcher view matcher to check for view
* @return view interaction
*/
public static TimedViewInteraction onViewWithTimeout(@NonNull final Matcher<View> viewMatcher) {
return onViewWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewMatcher);
}


/**
* A wrapper around {@link ViewInteraction} which allows to set timeouts for view actions and assertions.
*/
public static class TimedViewInteraction {


private ViewInteraction wrappedViewInteraction;


public TimedViewInteraction(ViewInteraction wrappedViewInteraction) {
this.wrappedViewInteraction = wrappedViewInteraction;
}


/**
* @see ViewInteraction#perform(ViewAction...)
*/
public TimedViewInteraction perform(final ViewAction... viewActions) {
wrappedViewInteraction.perform(viewActions);
return this;
}


/**
* {@link ViewInteraction#perform(ViewAction...)} with a timeout of {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
* @see ViewInteraction#perform(ViewAction...)
*/
public TimedViewInteraction performWithTimeout(final ViewAction... viewActions) {
return performWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewActions);
}


/**
* {@link ViewInteraction#perform(ViewAction...)} with a timeout.
* @see ViewInteraction#perform(ViewAction...)
*/
public TimedViewInteraction performWithTimeout(long timeoutInMillis, final ViewAction... viewActions) {
final long startTime = System.currentTimeMillis();
final long endTime = startTime + timeoutInMillis;


do {
try {
return perform(viewActions);
} catch (RuntimeException ex) {
//ignore
}


SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
}
while (System.currentTimeMillis() < endTime);


// timeout happens
throw new PerformException.Builder()
.withCause(new TimeoutException("Timeout occurred when trying to perform view actions: " + viewActions))
.build();
}


/**
* @see ViewInteraction#withFailureHandler(FailureHandler)
*/
public TimedViewInteraction withFailureHandler(FailureHandler failureHandler) {
wrappedViewInteraction.withFailureHandler(failureHandler);
return this;
}


/**
* @see ViewInteraction#inRoot(Matcher)
*/
public TimedViewInteraction inRoot(Matcher<Root> rootMatcher) {
wrappedViewInteraction.inRoot(rootMatcher);
return this;
}


/**
* @see ViewInteraction#check(ViewAssertion)
*/
public TimedViewInteraction check(final ViewAssertion viewAssert) {
wrappedViewInteraction.check(viewAssert);
return this;
}


/**
* {@link ViewInteraction#check(ViewAssertion)} with a timeout of {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
* @see ViewInteraction#check(ViewAssertion)
*/
public TimedViewInteraction checkWithTimeout(final ViewAssertion viewAssert) {
return checkWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewAssert);
}


/**
* {@link ViewInteraction#check(ViewAssertion)} with a timeout.
* @see ViewInteraction#check(ViewAssertion)
*/
public TimedViewInteraction checkWithTimeout(long timeoutInMillis, final ViewAssertion viewAssert) {
final long startTime = System.currentTimeMillis();
final long endTime = startTime + timeoutInMillis;


do {
try {
return check(viewAssert);
} catch (RuntimeException ex) {
//ignore
}


SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
}
while (System.currentTimeMillis() < endTime);


// timeout happens
throw new PerformException.Builder()
.withCause(new TimeoutException("Timeout occurred when trying to check: " + viewAssert.toString()))
.build();
}
}
}

然后在代码中调用它,例如:

onViewWithTimeout(withId(R.id.button).perform(click());

而不是

onView(withId(R.id.button).perform(click());

This also allows to you to add timeouts for view actions and view assertions.

你可以使用咖啡师的方法:

BaristaSleepActions.sleep(2000);

BaristaSleepActions.sleep(2, SECONDS);

Barista 是一个包装 Espresso 的库,以避免添加公认答案所需的所有代码。这里有一个链接!https://github.com/SchibstedSpain/Barista

我的实用程序重复执行可运行或可调用的执行,直到传递时没有错误或超时后抛出可抛出。 它在浓缩咖啡测试中完美无缺!

Suppose the last view interaction (button click) activates some background threads (network, database etc.). 因此,一个新的屏幕应该出现,我们想在下一步检查它, 但是我们不知道新的屏幕什么时候可以测试。

The recommended approach is to force your app to send messages about threads states to your test. 有时我们可以使用内置的机制,如 OkHttp3IdlingResource。 在其他情况下,您应该在应用程序源的不同位置插入代码片段(您应该知道应用程序逻辑!)只用于测试支持。 Moreover, we should turn off all your animations (although it's the part of UI).

另一种方法是等待,例如 SystemClock.sleep (10000)。但是我们不知道要等多久,甚至长时间的拖延也不能保证成功。 另一方面,你的测试会持续很长时间。

我的方法是为视图交互添加时间条件。例如,我们测试新的屏幕应该出现在10000毫秒(超时)。 但是我们不会等待并且尽快检查它(例如每100毫秒) 当然,我们以这种方式阻塞测试线程,但是通常,这正是我们在这种情况下所需要的。

Usage:


long timeout=10000;
long matchDelay=100; //(check every 100 ms)
EspressoExecutor myExecutor = new EspressoExecutor<ViewInteraction>(timeout, matchDelay);


ViewInteraction loginButton = onView(withId(R.id.login_btn));
loginButton.perform(click());


myExecutor.callForResult(()->onView(allOf(withId(R.id.title),isDisplayed())));

这是我的课程资料来源:

/**
* Created by alexshr on 02.05.2017.
*/


package com.skb.goodsapp;


import android.os.SystemClock;
import android.util.Log;


import java.util.Date;
import java.util.concurrent.Callable;


/**
* The utility repeats runnable or callable executing until it pass without errors or throws throwable after timeout.
* It works perfectly for Espresso tests.
* <p>
* Suppose the last view interaction (button click) activates some background threads (network, database etc.).
* As the result new screen should appear and we want to check it in our next step,
* but we don't know when new screen will be ready to be tested.
* <p>
* Recommended approach is to force your app to send messages about threads states to your test.
* Sometimes we can use built-in mechanisms like OkHttp3IdlingResource.
* In other cases you should insert code pieces in different places of your app sources (you should known app logic!) for testing support only.
* Moreover, we should turn off all your animations (although it's the part on ui).
* <p>
* The other approach is waiting, e.g. SystemClock.sleep(10000). But we don't known how long to wait and even long delays can't guarantee success.
* On the other hand your test will last long.
* <p>
* My approach is to add time condition to view interaction. E.g. we test that new screen should appear during 10000 mc (timeout).
* But we don't wait and check new screen as quickly as it appears.
* Of course, we block test thread such way, but usually it's just what we need in such cases.
* <p>
* Usage:
* <p>
* long timeout=10000;
* long matchDelay=100; //(check every 100 ms)
* EspressoExecutor myExecutor = new EspressoExecutor<ViewInteraction>(timeout, matchDelay);
* <p>
* ViewInteraction loginButton = onView(withId(R.id.login_btn));
* loginButton.perform(click());
* <p>
* myExecutor.callForResult(()->onView(allOf(withId(R.id.title),isDisplayed())));
*/
public class EspressoExecutor<T> {


private static String LOG = EspressoExecutor.class.getSimpleName();


public static long REPEAT_DELAY_DEFAULT = 100;
public static long BEFORE_DELAY_DEFAULT = 0;


private long mRepeatDelay;//delay between attempts
private long mBeforeDelay;//to start attempts after this initial delay only


private long mTimeout;//timeout for view interaction


private T mResult;


/**
* @param timeout     timeout for view interaction
* @param repeatDelay - delay between executing attempts
* @param beforeDelay - to start executing attempts after this delay only
*/


public EspressoExecutor(long timeout, long repeatDelay, long beforeDelay) {
mRepeatDelay = repeatDelay;
mBeforeDelay = beforeDelay;
mTimeout = timeout;
Log.d(LOG, "created timeout=" + timeout + " repeatDelay=" + repeatDelay + " beforeDelay=" + beforeDelay);
}


public EspressoExecutor(long timeout, long repeatDelay) {
this(timeout, repeatDelay, BEFORE_DELAY_DEFAULT);
}


public EspressoExecutor(long timeout) {
this(timeout, REPEAT_DELAY_DEFAULT);
}




/**
* call with result
*
* @param callable
* @return callable result
* or throws RuntimeException (test failure)
*/
public T call(Callable<T> callable) {
call(callable, null);
return mResult;
}


/**
* call without result
*
* @param runnable
* @return void
* or throws RuntimeException (test failure)
*/
public void call(Runnable runnable) {
call(runnable, null);
}


private void call(Object obj, Long initialTime) {
try {
if (initialTime == null) {
initialTime = new Date().getTime();
Log.d(LOG, "sleep delay= " + mBeforeDelay);
SystemClock.sleep(mBeforeDelay);
}


if (obj instanceof Callable) {
Log.d(LOG, "call callable");
mResult = ((Callable<T>) obj).call();
} else {
Log.d(LOG, "call runnable");
((Runnable) obj).run();
}
} catch (Throwable e) {
long remain = new Date().getTime() - initialTime;
Log.d(LOG, "remain time= " + remain);
if (remain > mTimeout) {
throw new RuntimeException(e);
} else {
Log.d(LOG, "sleep delay= " + mRepeatDelay);
SystemClock.sleep(mRepeatDelay);
call(obj, initialTime);
}
}
}
}

Https://gist.github.com/alexshr/ca90212e49e74eb201fbc976255b47e0

我对编程和浓缩咖啡还是个新手,所以虽然我知道最好的合理的解决方案是使用空转,但我还没有足够的智慧去做这些。

在我变得更加了解之前,我仍然需要我的测试以某种方式运行,所以现在我正在使用这个肮脏的解决方案,它尝试了很多次寻找一个元素,如果找到了就停止,如果没有,短暂的睡眠和重新开始,直到它达到最大的尝试次数(到目前为止最高的尝试次数是150次左右)。

private static boolean waitForElementUntilDisplayed(ViewInteraction element) {
int i = 0;
while (i++ < ATTEMPTS) {
try {
element.check(matches(isDisplayed()));
return true;
} catch (Exception e) {
e.printStackTrace();
try {
Thread.sleep(WAITING_TIME);
} catch (Exception e1) {
e.printStackTrace();
}
}
}
return false;
}

我在所有通过 ID、文本、父元素等查找元素的方法中都使用了这种方法:

static ViewInteraction findById(int itemId) {
ViewInteraction element = onView(withId(itemId));
waitForElementUntilDisplayed(element);
return element;
}

This is a helper I'm using in Kotlin for Android Tests. In my case I'm using the longOperation to mimic the server response but you can tweak it to your purpose.

@Test
fun ensureItemDetailIsCalledForRowClicked() {
onView(withId(R.id.input_text))
.perform(ViewActions.typeText(""), ViewActions.closeSoftKeyboard())
onView(withId(R.id.search_icon)).perform(ViewActions.click())
longOperation(
longOperation = { Thread.sleep(1000) },
callback = {onView(withId(R.id.result_list)).check(isVisible())})
}


private fun longOperation(
longOperation: ()-> Unit,
callback: ()-> Unit
){
Thread{
longOperation()
callback()
}.start()
}

你应该使用意式浓缩空转资源,这是建议在这个 代码实验室

An idling resource represents an asynchronous operation whose results affect subsequent operations in a UI test. By registering idling 资源,您可以验证这些异步 更可靠的操作时,测试您的应用程序。

来自 Presenter 的异步调用示例

@Override
public void loadNotes(boolean forceUpdate) {
mNotesView.setProgressIndicator(true);
if (forceUpdate) {
mNotesRepository.refreshData();
}


// The network request might be handled in a different thread so make sure Espresso knows
// that the app is busy until the response is handled.
EspressoIdlingResource.increment(); // App is busy until further notice


mNotesRepository.getNotes(new NotesRepository.LoadNotesCallback() {
@Override
public void onNotesLoaded(List<Note> notes) {
EspressoIdlingResource.decrement(); // Set app as idle.
mNotesView.setProgressIndicator(false);
mNotesView.showNotes(notes);
}
});
}

依赖性

androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'

For androidx

androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'

官方回购 https://github.com/googlecodelabs/android-testing :

IdlingResource 示例: < a href = “ https://github.com/googlesamples/android-testing/tree/master/ui/espresso/IdlingResourceSample”rel = “ nofollow noReferrer”> https://github.com/googlesamples/android-testing/tree/master/ui/espresso/idlingresourcesample

这与 this answer类似,但是使用超时而不是尝试,并且可以与其他 ViewInteractions 链接:

/**
* Wait for view to be visible
*/
fun ViewInteraction.waitUntilVisible(timeout: Long): ViewInteraction {
val startTime = System.currentTimeMillis()
val endTime = startTime + timeout


do {
try {
check(matches(isDisplayed()))
return this
} catch (e: AssertionFailedError) {
Thread.sleep(50)
}
} while (System.currentTimeMillis() < endTime)


throw TimeoutException()
}

用法:

onView(withId(R.id.whatever))
.waitUntilVisible(5000)
.perform(click())

我会加入我的方法:

fun suspendUntilSuccess(actionToSucceed: () -> Unit, iteration : Int = 0) {
try {
actionToSucceed.invoke()
} catch (e: Throwable) {
Thread.sleep(200)
val incrementedIteration : Int = iteration + 1
if (incrementedIteration == 25) {
fail("Failed after waiting for action to succeed for 5 seconds.")
}
suspendUntilSuccess(actionToSucceed, incrementedIteration)
}
}

叫做:

suspendUntilSuccess({
checkThat.viewIsVisible(R.id.textView)
})

You can add parameters like max iterations, iteration length, etc to the suspendUntilSuccess function.

I still prefer using idling resources, but when the tests are acting up due to slow animations on the device for instance, I use this function and it works well. It can of course hang for up to 5 seconds as it is before failing, so it could increase the execution time of your tests if the action to succeed never succeeds.

还可以使用 CountDownLatch 阻塞线程,直到收到来自服务器或超时的响应。

倒计时锁存器是一个简单而优雅的解决方案,无需外部库。它还可以帮助你专注于要测试的实际逻辑,而不是过度设计异步等待或等待响应

void testServerAPIResponse() {




Latch latch = new CountDownLatch(1);




//Do your async job
Service.doSomething(new Callback() {


@Override
public void onResponse(){
ACTUAL_RESULT = SUCCESS;
latch.countDown(); // notify the count down latch
// assertEquals(..
}


});


//Wait for api response async
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
assertEquals(expectedResult, ACTUAL_RESULT);


}