I like @jeprubio's answer above, however I ran into the same issue @desgraci mentioned in the comments, where their matcher is consistently looking for a view on an old, stale rootview. This happens frequently when trying to have transitions between activities in your test.
My implementation of the traditional "Implicit Wait" pattern lives in the two Kotlin files below.
EspressoExtensions.kt contains a function searchFor
which returns a ViewAction once a match has been found within supplied rootview.
class EspressoExtensions {
companion object {
/**
* Perform action of waiting for a certain view within a single root view
* @param matcher Generic Matcher used to find our view
*/
fun searchFor(matcher: Matcher<View>): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return isRoot()
}
override fun getDescription(): String {
return "searching for view $matcher in the root view"
}
override fun perform(uiController: UiController, view: View) {
var tries = 0
val childViews: Iterable<View> = TreeIterables.breadthFirstViewTraversal(view)
// Look for the match in the tree of childviews
childViews.forEach {
tries++
if (matcher.matches(it)) {
// found the view
return
}
}
throw NoMatchingViewException.Builder()
.withRootView(view)
.withViewMatcher(matcher)
.build()
}
}
}
}
}
BaseRobot.kt calls the searchFor()
method, checks if a matcher was returned. If no match is returned, it sleeps a tiny bit and then fetches a new root to match on until it has tried X times, then it throws an exception and the test fails. Confused about what a "Robot" is? Check out this fantastic talk by Jake Wharton about the Robot pattern. Its very similar to the Page Object Model pattern
open class BaseRobot {
fun doOnView(matcher: Matcher<View>, vararg actions: ViewAction) {
actions.forEach {
waitForView(matcher).perform(it)
}
}
fun assertOnView(matcher: Matcher<View>, vararg assertions: ViewAssertion) {
assertions.forEach {
waitForView(matcher).check(it)
}
}
/**
* Perform action of implicitly waiting for a certain view.
* This differs from EspressoExtensions.searchFor in that,
* upon failure to locate an element, it will fetch a new root view
* in which to traverse searching for our @param match
*
* @param viewMatcher ViewMatcher used to find our view
*/
fun waitForView(
viewMatcher: Matcher<View>,
waitMillis: Int = 5000,
waitMillisPerTry: Long = 100
): ViewInteraction {
// Derive the max tries
val maxTries = waitMillis / waitMillisPerTry.toInt()
var tries = 0
for (i in 0..maxTries)
try {
// Track the amount of times we've tried
tries++
// Search the root for the view
onView(isRoot()).perform(searchFor(viewMatcher))
// If we're here, we found our view. Now return it
return onView(viewMatcher)
} catch (e: Exception) {
if (tries == maxTries) {
throw e
}
sleep(waitMillisPerTry)
}
throw Exception("Error finding a view matching $viewMatcher")
}
}
To use it
// Click on element withId
BaseRobot().doOnView(withId(R.id.viewIWantToFind, click())
// Assert element withId is displayed
BaseRobot().assertOnView(withId(R.id.viewIWantToFind, matches(isDisplayed()))
I know that IdlingResource is what Google preaches to handle asynchronous events in Espresso testing, but it usually requires that you have test specific code (i.e hooks) embedded within your app code in order to synchronize the tests. That seems weird to me, and working on a team with a mature app and multiple developers committing code everyday, it seems like it would be a lot of extra work to retrofit idling resources everywhere in the app just for the sake of tests. Personally, I prefer to keep the app and test code as separate as possible.
/end rant
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…