我正在尝试使用 Espresso 自动化一个 Android 应用程序,该应用程序是一个聊天机器人。现在我正在等待中挣扎。如果我使用
Thread.sleep
,它工作得很好。但是,我想等到屏幕上出现特定文本。我怎样才能做到这一点?
@Rule
public ActivityTestRule<LoginActivity> mActivityTestRule = new ActivityTestRule<>(LoginActivity.class);
@Test
public void loginActivityTest() {
ViewInteraction loginName = onView(allOf(withId(R.id.text_edit_field),
childAtPosition(childAtPosition(withId(R.id.email_field),0), 1)));
loginName.perform(scrollTo(), replaceText("[email protected]"), closeSoftKeyboard());
ViewInteraction password= onView(allOf(withId(R.id.text_edit_field),
childAtPosition(childAtPosition(withId(R.id.password_field),0), 1)));
password.perform(scrollTo(), replaceText("12345678"), closeSoftKeyboard());
ViewInteraction singInButton = onView(allOf(withId(R.id.sign_in), withText("Sign In"),childAtPosition(childAtPosition(withId(R.id.scrollView), 0),2)));
singInButton .perform(scrollTo(), click());
//Here I need to wait for the text "Hi ..."
一些解释:按下登录按钮后,聊天机器人会说“嗨”并提供更多信息。我想等待最后一条消息出现在屏幕上。
我喜欢@jeprubio上面的答案,但是我遇到了@desgraci在评论中提到的同样的问题,他们的匹配器一直在寻找旧的、陈旧的根视图的视图。当尝试在测试中的活动之间进行转换时,这种情况经常发生。
我对传统“隐式等待”模式的实现位于下面的两个 Kotlin 文件中。
EspressoExtensions.kt 包含一个函数
searchFor
,一旦在提供的根视图中找到匹配项,该函数就会返回 ViewAction。
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 调用
searchFor()
方法,检查是否返回匹配器。如果没有返回匹配项,它会休眠一小会儿,然后获取新的根进行匹配,直到尝试了 X 次,然后它会抛出异常并且测试失败。对“机器人”是什么感到困惑吗?查看 Jake Wharton 的精彩演讲 关于机器人模式。它与页面对象模型模式非常相似
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")
}
}
使用方法
// Click on element withId
BaseRobot().doOnView(withId(R.id.viewIWantToFind), click())
// Assert element withId is displayed
BaseRobot().assertOnView(withId(R.id.viewIWantToFind), matches(isDisplayed()))
我知道 IdlingResource 是 Google 在 Espresso 测试中处理异步事件的方法,但它通常要求您在应用程序代码中嵌入测试特定代码(即挂钩),以便同步测试。这对我来说似乎很奇怪,并且在一个拥有成熟应用程序和多个开发人员每天提交代码的团队中工作,似乎仅仅为了测试而改造应用程序中各处的闲置资源将需要大量额外的工作。就我个人而言,我更喜欢将应用程序和测试代码尽可能分开。 /吐槽结束
您可以创建一个空闲资源或使用自定义的ViewAction,如下所示:
/**
* Perform action of waiting for a specific view id.
* @param viewId The id of the view to wait for.
* @param millis The timeout of until when to wait for.
*/
public static ViewAction waitId(final int viewId, final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
}
@Override
public String getDescription() {
return "wait for a specific view with id <" + viewId + "> during " + millis + " millis.";
}
@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher = withId(viewId);
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
// found view with required ID
if (viewMatcher.matches(child)) {
return;
}
}
uiController.loopMainThreadForAtLeast(50);
}
while (System.currentTimeMillis() < endTime);
// timeout happens
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
你可以这样使用它:
onView(isRoot()).perform(waitId(R.id.theIdToWaitFor, 5000));
使用特定 ID 更改
theIdToWaitFor
,并根据需要更新 5 秒(5000 毫秒)的超时。
如果您正在等待的文本位于
TextView
中,直到登录完成后才会进入视图层次结构,那么我建议使用此线程中在根视图(即此处或此处)。
但是,如果您正在等待视图层次结构中已存在的
TextView
中的文本发生更改,那么我强烈建议定义一个对 ViewAction
本身进行操作的 TextView
,以便更好地测试输出测试失败的情况。
定义一个对特定
ViewAction
进行操作的 TextView
,而不是对根视图进行操作,过程分为三个步骤,如下所示。
首先定义
ViewAction
类如下:
/**
* A [ViewAction] that waits up to [timeout] milliseconds for a [View]'s text to change to [text].
*
* @param text the text to wait for.
* @param timeout the length of time in milliseconds to wait for.
*/
class WaitForTextAction(private val text: String,
private val timeout: Long) : ViewAction {
override fun getConstraints(): Matcher<View> {
return isAssignableFrom(TextView::class.java)
}
override fun getDescription(): String {
return "wait up to $timeout milliseconds for the view to have text $text"
}
override fun perform(uiController: UiController, view: View) {
val endTime = System.currentTimeMillis() + timeout
do {
if ((view as? TextView)?.text == text) return
uiController.loopMainThreadForAtLeast(50)
} while (System.currentTimeMillis() < endTime)
throw PerformException.Builder()
.withActionDescription(description)
.withCause(TimeoutException("Waited $timeout milliseconds"))
.withViewDescription(HumanReadables.describe(view))
.build()
}
}
其次,定义一个包装此类的辅助函数,如下所示:
/**
* @return a [WaitForTextAction] instance created with the given [text] and [timeout] parameters.
*/
fun waitForText(text: String, timeout: Long): ViewAction {
return WaitForTextAction(text, timeout)
}
第三,也是最后,调用辅助函数如下:
onView(withId(R.id.someTextView)).perform(waitForText("Some text", 5000))