Unveiling Asynchrony: Lessons from Dark Matter, Berlin’s Hidden Gem

Joseph James (JJ)
3 min readJan 14, 2024

--

Photo by Joseph James — Escape from reality ✨

On the outskirts of Berlin, there’s a place that seems to bend the very fabric of reality — Dark Matter. Here, individuals are bathed in the glow of fluorescent light, much like coroutines in an Android application, silently executing tasks in the background. This scene is a perfect metaphor for the world of Android UI testing, where Espresso and coroutines coexist, unseen yet integral.

The Realm of Idling Resources: A Personal Journey

My exploration into UI tests began about a year and a half ago, focusing on a feature involving camera interaction and image processing. Here’s how the Page Object Model of the UI tests were structured:

internal class ImageCaptureScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<CameraScreen>(semanticsProvider = semanticsProvider) {
private val shutterButton: KNode
get() = child { hasTestTag(TestTags.CAMERA_SHUTTER_BUTTON) }
fun clickImageCaptureButton() {
shutterButton.performClick()
}
}

internal fun onImageCaptureScreen(
composeTestRule: SemanticsNodeInteractionsProvider,
function: CameraScreen.() -> Unit
) = ComposeScreen.onComposeScreen(composeTestRule, function)
internal class ImageReviewScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<CameraScreen>(semanticsProvider = semanticsProvider) {
private val callApiButton: KNode
get() = child { hasText("submit") }
fun clickCallApiButton() {
callApiButton.performClick()
}
}

internal fun onImageReviewScreen(
composeTestRule: SemanticsNodeInteractionsProvider,
function: CameraScreen.() -> Unit
) = ComposeScreen.onComposeScreen(composeTestRule, function)

The user flow involved clicking the image capture button, saving the image internally, and then calling the API on the next screen. But a challenge arose when my PR began failing due to a coroutine running alongside IO operations.

I learned about idling resources, which allow UI test threads to pause until processes like image capturing are complete. We implemented an idling resource at the start and end of the image-capturing process to resolve this issue.

Thinking from a much broader perspective, instead of writing idling resources to all the coroutine implementations and to bridge this gap and bring these background activities into focus, we can use custom coroutine dispatchers. This integration allows Espresso to be aware of and sync with the coroutine activities, ensuring more reliable and accurate UI tests.

Constructing the Custom Coroutine Dispatcher

We created a dispatcher that liaises with CountingIdlingResource:

internal class IdlingResourceDispatcher(
private val baseDispatcher: CoroutineDispatcher,
private val idlingResource: CountingIdlingResource
) : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {

idlingResource.increment()

baseDispatcher.dispatch(context, Runnable {
try {
block.run()
} finally {
idlingResource.decrement()
}
})
}
}

Providing test dispatcher globally

class MyDispatcherProvider(
private val countingIdlingResource: CountingIdlingResource
): DispatcherProvider {
override val main: CoroutineDispatcher = IdlingResourceDispatcher(Dispatchers.Main, countingIdlingResource)
override val io: CoroutineDispatcher = IdlingResourceDispatcher(Dispatchers.IO, countingIdlingResource)
}

Preparing for Test Scenarios

Before conducting tests, it’s crucial to register our CountingIdlingResource with Espresso

Espresso.registerIdlingResources(countingIdlingResource)

** Registering the idling resource could also be done via test rules

The approach simplifies the process of writing UI tests and ensures that each dispatcher created within the codebase is already tracked by the idling resource, giving us much greater control over the tests.

Conclusion

Much like the enigmatic Dark Matter in Berlin, the world of Android UI testing with coroutines and Espresso’s Idling Resources is full of hidden complexities. In the end, the challenges faced in coding, like those in exploring unknown places, provide invaluable opportunities to learn and grow. As we navigate through the complexities of asynchrony in app development, we continue to uncover the mysteries and potential of our digital creations, much like the explorers of the fluorescent-lit corners of Dark Matter.

Happy coding and exploring! 🚀

--

--

Joseph James (JJ)
Joseph James (JJ)

No responses yet