javaandroid-studiouser-interfacetestingandroid-espresso

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


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

Solution

  • There are several options:

    1) Repeated retries with Awaitility

    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).

    2) Repeated retries with Awaitility, and an initial time delay

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

    3) Simple time delay

    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:


    4) Use Espresso or Barista functions to create the timed waits:


    5) Use Espresso Idling Resources


    6) Use Jetpack Compose wait() functions

    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
    )
    

    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.