Android Espresso等待文本显示

时间:2018-04-12 12:08:02

标签: android chatbot android-espresso

我正在尝试使用Espresso自动化Android应用程序,即聊天机器人。我可以说我是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("test@test.test"), 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());

//我需要等待文字“嗨......” //一些解释:按下按钮后,聊天机器人说“嗨”,提供更多信息,我想等待最后一条消息出现在屏幕上。

3 个答案:

答案 0 :(得分:5)

您可以创建idling resource或使用自定义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毫秒)。

答案 1 :(得分:0)

我喜欢@jeprubio的回答,但是遇到了评论中提到的同一问题@desgraci,他们的匹配者一直在寻找旧的,过时的rootview的视图。尝试在测试中的活动之间进行转换时,经常会发生这种情况。

我对传统“隐式等待”模式的实现位于下面的两个Kotlin文件中。

EspressoExtensions.kt 包含函数searchFor,一旦在提供的rootview中找到匹配项,该函数就会返回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次,然后抛出异常,测试失败。对什么是“机器人”感到困惑?查阅this fantastic talk by 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测试中处理异步事件的方法,但是通常它要求您在应用程序代码中嵌入特定于测试的代码(即挂钩),才能使测试同步。对于我来说,这似乎很奇怪,并且与一个拥有成熟应用程序且每天都有多个开发人员提交代码的团队一起工作,似乎只是为了进行测试而在应用程序中各处闲置资源进行改造将是很多额外的工作。就我个人而言,我更喜欢将应用程序和测试代码尽可能地分开。 / end rant

答案 2 :(得分:0)

如果您正在等待的文本位于TextView中,直到登录完成后才进入视图层次结构,那么我建议在此线程中使用其他答案之一在根视图(即herehere)上运行。

但是,如果您正在等待视图层次结构中已经存在的TextView中的文本更改,那么我强烈建议定义一个在{{1 }}本身,以在测试失败的情况下提供更好的测试输出。

定义一个在特定ViewAction上运行而不是在根视图上运行的TextView的过程分为以下三个步骤。

首先,如下定义ViewAction类:

TextView

第二,定义一个包装此类的辅助函数,如下所示:

ViewAction

第三次也是最后一次,调用helper函数,如下所示:

/**
 * 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()
    }
}