Espresso Idling Resources: Synchronizing Tests with Async Operations
Espresso's automatic synchronization is its killer feature: tests wait automatically until the main thread is idle before performing actions. But when your app does work on background threads — network calls, database operations, coroutines — Espresso doesn't automatically know when that work is done. IdlingResource solves this.
How Espresso Synchronization Works
By default, Espresso waits for:
- The main thread message queue to be empty
- No animations in progress
AsyncTaskthread pools to be idle
This covers many cases, but not all. Retrofit network calls, RxJava observables, Kotlin coroutines, and WorkManager jobs run outside the main thread and outside AsyncTask pools. Espresso doesn't wait for these automatically — tests run before the async work completes, causing flaky failures.
What Is IdlingResource?
IdlingResource is an interface that tells Espresso when your app is busy:
interface IdlingResource {
fun getName(): String
fun isIdleNow(): Boolean
fun registerIdleTransitionCallback(callback: ResourceCallback)
}When registered, Espresso checks isIdleNow() before every action. If isIdleNow() returns false, Espresso waits. When the resource becomes idle, it calls callback.onTransitionToIdle() to notify Espresso.
SimpleCountingIdlingResource
The simplest implementation counts in-flight operations:
import androidx.test.espresso.idling.CountingIdlingResource
// Create once, share across app
val countingIdlingResource = CountingIdlingResource("NetworkCalls")
// In your network layer:
fun makeRequest() {
countingIdlingResource.increment() // Busy
apiService.getData().enqueue(object : Callback<Response> {
override fun onResponse(call: Call<Response>, response: Response<Response>) {
countingIdlingResource.decrement() // Idle
}
override fun onFailure(call: Call<Response>, t: Throwable) {
countingIdlingResource.decrement() // Also idle on failure
}
})
}
// In tests:
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(countingIdlingResource)
}
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(countingIdlingResource)
}OkHttp Idling Resource
For teams using OkHttp with Retrofit, the espresso-idling-resource library includes OkHttpIdlingResource:
androidTestImplementation 'com.jakewharton.espresso:okhttp3-idling-resource:1.0.0'// In your test setup
@Before
fun setupIdlingResource() {
val okHttpClient = (retrofitInstance as? RetrofitWrapper)?.okHttpClient
if (okHttpClient != null) {
idlingResource = OkHttp3IdlingResource.create("OkHttp", okHttpClient)
IdlingRegistry.getInstance().register(idlingResource)
}
}This tracks all in-flight OkHttp requests automatically — no changes to your production network code needed.
Kotlin Coroutines Idling Resource
Coroutines need special handling. Use IdlingCoroutineDispatcher:
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'Approach 1: Replace CoroutineDispatcher in Tests
Inject a CoroutineDispatcher into your ViewModel/Repository:
// Production code
class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun getUser(id: Int): User = withContext(ioDispatcher) {
api.getUser(id)
}
}
// Test code
import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor
import kotlinx.coroutines.asCoroutineDispatcher
class UserRepositoryTest {
private lateinit var idlingDispatcher: CoroutineDispatcher
private lateinit var repository: UserRepository
@Before
fun setup() {
val executor = Executors.newSingleThreadExecutor()
idlingDispatcher = executor.asCoroutineDispatcher()
repository = UserRepository(idlingDispatcher)
}
}Approach 2: AsyncTaskIdlingResource for Simple Cases
For simple scenarios where you just need to wait for a coroutine to complete:
// Custom coroutine idling resource
class CoroutineIdlingResource(private val name: String) : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null
private val counter = AtomicInteger(0)
override fun getName() = name
override fun isIdleNow() = counter.get() == 0
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
fun increment() {
counter.incrementAndGet()
}
fun decrement() {
if (counter.decrementAndGet() == 0) {
callback?.onTransitionToIdle()
}
}
}
// Usage with coroutines
class MyViewModel(private val repository: UserRepository) : ViewModel() {
val idlingResource = CoroutineIdlingResource("ViewModelWork")
fun loadUser(id: Int) {
idlingResource.increment()
viewModelScope.launch {
val user = repository.getUser(id)
_userState.value = user
idlingResource.decrement()
}
}
}RxJava Idling Resource
For RxJava, use the rx-idler library:
androidTestImplementation 'com.squareup.rx.idler:rx2-idler:0.11.0'
// Or for RxJava 3:
androidTestImplementation 'com.squareup.rx.idler:rx3-idler:0.11.0'// In your test Application or test setup
class TestApplication : Application() {
override fun onCreate() {
super.onCreate()
// Replace RxJava schedulers with idling versions
RxJavaPlugins.setInitIoSchedulerHandler {
Rx2Idler.wrap(Schedulers.io(), "RxJava2 IO Scheduler")
}
RxJavaPlugins.setInitComputationSchedulerHandler {
Rx2Idler.wrap(Schedulers.computation(), "RxJava2 Computation Scheduler")
}
}
}This wraps all RxJava schedulers automatically — no per-test configuration needed.
WorkManager Idling Resource
For testing code that triggers WorkManager jobs:
androidTestImplementation 'androidx.work:work-testing:2.9.0'@Before
fun setup() {
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(SynchronousExecutor()) // Makes work synchronous in tests
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
}
@Test
fun syncJob_completesSuccessfully() {
val request = OneTimeWorkRequestBuilder<SyncWorker>().build()
WorkManager.getInstance(context).enqueue(request)
// With SynchronousExecutor, work runs synchronously
val workInfo = WorkManager.getInstance(context)
.getWorkInfoById(request.id)
.get()
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.state)
}Custom IdlingResource for Network
When you can't use OkHttp directly, implement a custom IdlingResource based on your app's loading state:
// Observes a LiveData loading flag
class LoadingStateIdlingResource(
private val name: String,
private val viewModel: MyViewModel
) : IdlingResource, Observer<Boolean> {
private var callback: IdlingResource.ResourceCallback? = null
private var isLoading = false
init {
// Register as observer on the main thread
viewModel.isLoading.observeForever(this)
}
override fun getName() = name
override fun isIdleNow() = !isLoading
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
override fun onChanged(loading: Boolean) {
isLoading = loading
if (!loading) {
callback?.onTransitionToIdle()
}
}
fun tearDown() {
viewModel.isLoading.removeObserver(this)
}
}
// In test
private lateinit var idlingResource: LoadingStateIdlingResource
@Before
fun setup() {
activityRule.scenario.onActivity { activity ->
val viewModel = ViewModelProvider(activity)[MyViewModel::class.java]
idlingResource = LoadingStateIdlingResource("LoadingState", viewModel)
IdlingRegistry.getInstance().register(idlingResource)
}
}
@After
fun tearDown() {
IdlingRegistry.getInstance().unregister(idlingResource)
idlingResource.tearDown()
}Avoiding Idling Resources: Alternative Patterns
Sometimes there are simpler alternatives:
Using waitFor with PollingCheck
For situations where you can't register an idling resource:
// Wait for a view to appear (polling approach)
fun waitForView(viewMatcher: Matcher<View>, timeout: Long = 5000) {
val startTime = SystemClock.uptimeMillis()
while (SystemClock.uptimeMillis() - startTime < timeout) {
try {
onView(viewMatcher).check(matches(isDisplayed()))
return // Success
} catch (e: NoMatchingViewException) {
Thread.sleep(100)
}
}
// Final check — will throw if still not visible
onView(viewMatcher).check(matches(isDisplayed()))
}
// Usage
waitForView(withText("Data loaded"))This is less elegant than IdlingResource but sometimes practical for legacy code you can't modify.
Making Tests Deterministic
The best async synchronization is no synchronization needed. Use test doubles that return synchronously:
// Fake repository that returns immediately
class FakeUserRepository : UserRepository {
override suspend fun getUser(id: Int): User = User(id, "Test User", "test@example.com")
override suspend fun saveUser(user: User) { /* no-op */ }
}
// Inject in test
@Before
fun setup() {
// Hilt or manual DI to inject fake
val repository = FakeUserRepository()
viewModel = UserViewModel(repository)
}When dependencies return synchronously, coroutines complete synchronously, and Espresso synchronization works without IdlingResource.
Debugging Idling Resource Issues
When tests fail with AppNotIdleException:
- Check for leaked IdlingResources: Always unregister in
@After - Check for animation leaks: Disable animations in developer options for CI
- Check for background services: Services running in the background can keep the app busy
- Log idle state changes: Add logging in
isIdleNow()andonTransitionToIdle()
override fun isIdleNow(): Boolean {
val idle = counter.get() == 0
Log.d("IdlingResource", "$name isIdleNow=$idle (count=${counter.get()})")
return idle
}Summary
IdlingResource is Espresso's mechanism for telling the framework about async work it can't automatically detect. The key patterns:
- Use
CountingIdlingResourcefor explicit increment/decrement control - Use
OkHttp3IdlingResourcefor Retrofit/OkHttp apps (zero production code changes) - Use
rx-idlerfor RxJava schedulers (zero production code changes) - For Coroutines, inject dispatchers and replace with synchronous ones in tests, or implement a counting idling resource
- Always unregister IdlingResources in
@After— leaked resources cause test isolation failures
The ideal is to minimize the need for IdlingResources by using synchronous test doubles for data layer dependencies. But for integration-level Espresso tests that exercise the real network or database layer, IdlingResources are the correct tool.