Asynchronous Android screens often finish work after Espresso has already found a view, which can leave assertions racing network, database, or coroutine state. An IdlingResource gives Espresso a named busy-or-idle signal for app-owned work so the test waits for the screen to become assertable instead of sleeping blindly.
A debug-only wrapper keeps the Espresso idling dependency out of the release APK. The app code calls that wrapper around the asynchronous operation, the debug variant exposes a CountingIdlingResource, and the release variant keeps the same app-facing function as a no-op.
Register the resource in the instrumentation test before the action that starts the work, and unregister it after the test finishes. A missing decrement leaves Espresso waiting until its idling timeout, so the final connected test run should pass without any fixed Thread.sleep guard.
Related: How to wait for UI elements in Espresso
Related: How to debug a flaky Espresso test
Related: How to set Espresso idling timeouts
dependencies { androidTestImplementation( "androidx.test.espresso:espresso-core:3.7.0" ) debugImplementation( "androidx.test.espresso:espresso-idling-resource:3.7.0" ) }
Use the project version catalog if the build already manages AndroidX Test versions there. The debugImplementation dependency keeps CountingIdlingResource out of release builds.
package com.example.orders.testing import androidx.test.espresso.IdlingResource import androidx.test.espresso.idling.CountingIdlingResource object OrderIdlingResource { private val counter = CountingIdlingResource("OrderRepository") val idlingResource: IdlingResource = counter suspend fun <T> track( block: suspend () -> T ): T { counter.increment() return try { block() } finally { counter.decrement() } } }
Place this file under src/debug/kotlin. Name the counter after the app component that owns the asynchronous work because that name appears in Espresso timeout diagnostics.
package com.example.orders.testing object OrderIdlingResource { suspend fun <T> track( block: suspend () -> T ): T = block() }
Place this file under src/release/kotlin. Keep the package, object name, and track function signature identical across build variants so main source compiles for both debug and release.
package com.example.orders import com.example.orders.testing.OrderIdlingResource class OrderRepository(private val api: OrderApi) { suspend fun refreshStatus(orderId: String): OrderStatus = OrderIdlingResource.track { api.fetchStatus(orderId) } }
For callback-based code, increment before starting the request and decrement in the success, error, or cancellation callback. Decrementing immediately after scheduling the request marks the resource idle too early.
package com.example.orders import androidx.test.espresso.Espresso.onView import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import com.example.orders.testing.OrderIdlingResource import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class OrderStatusTest { @Before fun registerIdlingResource() { IdlingRegistry.getInstance().register( OrderIdlingResource.idlingResource ) } @After fun unregisterIdlingResource() { IdlingRegistry.getInstance().unregister( OrderIdlingResource.idlingResource ) } @Test fun refreshShowsReadyStatus() { onView(withId(R.id.refreshButton)).perform(click()) onView(withId(R.id.status)).check(matches(withText("Ready"))) } }
Always unregister the resource in @After. A registered resource that keeps state between tests can make an unrelated test wait or time out.
// Remove this after the IdlingResource is registered. Thread.sleep(5_000)
If the assertion still needs a fixed sleep, the app operation that changes the view is not fully covered by the resource. Use a bounded view wait only for view-tree state that is not controlled by background app work.
$ ./gradlew :app:connectedDebugAndroidTest Task :app:connectedDebugAndroidTest Starting 1 tests on Pixel_8_API_35(AVD) - 15 OrderStatusTest > refreshShowsReadyStatus PASSED BUILD SUCCESSFUL in 31s
Replace :app and Debug with the module and variant used by the project. Android's command-line test workflow writes the connected test report under the module build reports directory.