I'm using Espresso to write some automated tests for an Android app that I've developed. All the tests are automated and are passing/failing according to what happens with the UI. I've ran the code through SonarQube to detect bad coding practices and it's informed me that Thread.Sleep() should not be used.
I'm mainly using Thread.sleep() in instances where I'm typing out a form and need to hide the keyboard to scroll down to tap the next form field etc. From my understanding, using something like awaitility is for big async functions like fetching data etc. but what should I use in my case where something is not being fetched but more so for just interacting with the UI?
Here is an example of a log in test that I have created that uses Thread.Sleep():
onView(withId(R.id.fieldEmail)).perform(typeText("shelley@gmail.com"));
Thread.sleep(SHORT_WAIT);
onView(withId(R.id.fieldPassword)).perform(click());
onView(withId(R.id.fieldPassword)).perform(typeText("password"));
Thread.sleep(SHORT_WAIT);
onView(isRoot()).perform(pressBack());
Thread.sleep(SHORT_WAIT);
onView(withId(R.id.signIn)).perform(click());
Thread.sleep(LONG_WAIT);
There are several options:
You can use Awaitility to repeatedly retry an assertion/check, up to a specified time allowance:
app/build.gradle
dependencies {
// If you're using androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
// and compiling your project with Java 8 or above, use version 4.2.0+. But note that
// that you must run the UI tests on Android 9 (API 28) or above, which has the required
// Java 8 classes, such as java.time.temporal.ChronoUnit
androidTestImplementation 'org.awaitility:awaitility:4.2.0'
// If you're using androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0",
// or running the UI tests on Android 7.1 or below, then you must use Awaitility
// version 3, otherwise you will get dependency conflicts with JUnit's Hamcrest.
// See: https://github.com/awaitility/awaitility/issues/194
androidTestImplementation 'org.awaitility:awaitility:3.1.6'
}
import java.util.concurrent.TimeUnit;
// Set the retry time to 0.5 seconds, instead of the default 0.1 seconds
Awaitility.setDefaultPollInterval(500, TimeUnit.MILLISECONDS);
// Kotlin:
Awaitility.await().atMost(4, TimeUnit.SECONDS).untilAsserted {
onView(withId(R.id.fieldPassword)).perform(click())
}
// Java 8:
Awaitility.await().atMost(4, TimeUnit.SECONDS).untilAsserted(() ->
onView(withId(R.id.fieldPassword)).perform(click())
);
// Java 7:
Awaitility.await().atMost(4, 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 4 seconds, until it's true
or until a timeout happens (fail
).
You can also set an initial time delay before making the first assertion:
import java.util.concurrent.TimeUnit
Awaitility
.await()
.pollInterval(500, TimeUnit.MILLISECONDS)
.pollDelay(1, TimeUnit.SECONDS)
.atMost(3, TimeUnit.SECONDS)
.untilAsserted { onView(withId(R.id.fieldPassword)).perform(click()) }
Alternatively, you can add simple time delays in between statements, just like Thread.sleep()
, but in a more verbose way:
import java.util.concurrent.TimeUnit;
// Kotlin:
Awaitility.await().pollDelay(2, TimeUnit.SECONDS).until { true }
// Java 8
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;
}
});
More info about Awaitility:
For Composable UI tests, use composeTestRule.waitForIdle()
and composeTestRule.waitUntil()
and composeTestRule.waitUntilExactlyOneExists()
. This will cause the test runner to wait until a long operation finishes. An example is waiting for the on-screen keyboard to appear or disappear.
composeTestRule.onNodeWithTag(TextInputTag.TEXT_ENTRY_FIELD).performClick()
// Wait for the on-screen keyboard to display, which can take up to 3 seconds
composeTestRule.waitForIdle()
// Assert something after the keyboard has appeared
composeTestRule.onNodeWithTag(TextInputTag.TEXT_ENTRY_FIELD).assert...()
val THREE_SECONDS_TIMEOUT = 3000L
composeTestRule.onNodeWithTag(TextInputTag.TEXT_ENTRY_FIELD).performClick()
// Wait for the user login to finish
composeTestRule.waitUntil(THREE_SECONDS_TIMEOUT) { isUserLoggedIn() }
// Assert something after the user has logged in
composeTestRule.onNodeWithTag(TextInputTag.TEXT_ENTRY_FIELD).assert...()
@OptIn(ExperimentalTestApi::class) // Add this to your test class or test function
// Wait until a text entry field appears on the screen
composeTestRule.waitUntilExactlyOneExists(
matcher = hasTestTag(TextInputTag.TEXT_ENTRY_FIELD),
timeoutMillis = THREE_SECONDS_TIMEOUT
)
waitForIdle()
here.waitUntil()
here.waitUntilExactlyOneExists()
here.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.