Espresso Idling Resources: Synchronizing Tests with Async Operations

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
  • AsyncTask thread 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:

  1. Check for leaked IdlingResources: Always unregister in @After
  2. Check for animation leaks: Disable animations in developer options for CI
  3. Check for background services: Services running in the background can keep the app busy
  4. Log idle state changes: Add logging in isIdleNow() and onTransitionToIdle()
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 CountingIdlingResource for explicit increment/decrement control
  • Use OkHttp3IdlingResource for Retrofit/OkHttp apps (zero production code changes)
  • Use rx-idler for 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.

Read more

Start now free