Espresso: Thread.Sleep( )

Espresso: Thread.sleep( )

On my mind correct approach will be:

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

And then pattern of usage will be:

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

How is uiController.loopMainThreadForAtLeast different from Thread.sleep?

Espresso uses a queue to check if app is idle.

Thread.sleep(x) = //Waits x 
uiController.loopMainThreadForAtLeast(x) // Waits x + more if app is still not idle

Now both of this are really bad approaches, that will result in some problems in production code.

  1. Espresso can detect if app is idle if you are using AsyncTasks
    but in some scenarios it can't. So even when your app isn't idle,
    uiController.loopMainThreadForAtLeast(x) will be skipped.

  2. Using IdlingResources inside production code to specify when
    app is not idle is not a good solution. You shouldn't change
    production code to test your app and idling resources checks if app
    is idle with 5 second intervals which will cause lots of overhead in
    your tests. Assume you have 100 test cases where you will experience
    this interval, the overhead you will experience will be huge

  3. Using Thread.sleep(x)by itself is obviously a bad idea. As your
    tests needs to be stable regardless of the speed of internet
    connection, phone etc you shouldn't be waiting a fixed amount of
    time. And you can't know how much time you need to wait when you
    request something from a server so x you decide will either be too
    big or too small.

The best solution is to check if the target view you have in your assertion/action exists and if not check the target view again after X seconds (where X is a small number like 200 millis). Also using Thread.sleep() worked better for me than using
uiController.loopMainThreadForAtLeast() when my app was showing animations (even when animations were closed from testOptions and phone settings)

What is the best way to await functions in UI testing in Android Studio without Thread.Sleep()?

There are several options:

Repeated retries

You can use Awaitility to repeatedly retry an assertion/check, up to a specified time allowance:

app/build.gradle

dependencies {
// Note: Awaitility version 4 has dependency conflicts with JUnit's
// Hamcrest. See: https://github.com/awaitility/awaitility/issues/194
androidTestImplementation 'org.awaitility:awaitility:3.1.6'
}
// Set the retry time to 0.5 seconds, instead of the default 0.1 seconds
Awaitility.setDefaultPollInterval(500, TimeUnit.MILLISECONDS);

Kotlin:
Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted {
onView(withId(R.id.fieldPassword)).perform(click())
}

Java 7:
Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(new ThrowingRunnable() {
@Override
public void run() throws Throwable {
onView(withId(R.id.fieldPassword)).perform(click());
}
});

This means that if the assertion fails the first time, it will retry for up to 2 seconds, until it's true or until a timeout happens (fail).

Repeated retries with an initial time delay

You can also set an initial time delay before making the first assertion:

Awaitility.await().pollDelay(1, TimeUnit.SECONDS).atMost(3, TimeUnit.SECONDS).untilAsserted {
onView(withId(R.id.fieldPassword)).perform(click())
}

Simple time delay

Alternatively, you can add simple time delays in between statements, just like Thread.sleep(), but in a more verbose way:

Kotlin:
Awaitility.await().pollDelay(2, TimeUnit.SECONDS).until { true }

Java 7:
Awaitility.await().pollDelay(2, TimeUnit.SECONDS).until(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return true;
}
});

Use Espresso or Barista functions to create the timed waits:

  • Espresso: Thread.sleep( )

Use Espresso Idling Resources

  • https://dev.to/keyopstech/robust-ui-tests-with-espresso-and-idling-resources-157f
  • How to use Espresso Idling Resource for network calls
  • https://tudip.com/blog-post/espresso-idling-resources/
  • https://medium.com/androiddevelopers/android-testing-with-espressos-idling-resources-and-testing-fidelity-8b8647ed57f4
  • https://www.repeato.app/the-problem-of-espresso-idling-resources/ (some disadvantages)

General note: Although using Thread.sleep() or some other time delay should normally be avoided in unit tests, there will be times where you need to use it, and there is no alternative. An example is using IntentsTestRule to click on a web link in your app to launch the external web browser. You don't know how long the browser will take to launch the web page, so you need to add a time delay.


More info about Awaitility:

  • https://medium.com/stepstone-tech/taming-the-ui-test-monster-26c017848ae0
  • https://www.jfokus.se/jfokus12/preso/jf12_StopSleepingStartAwaiting.pdf
  • https://github.com/awaitility/awaitility/wiki/Usage


Related Topics



Leave a reply



Submit