【问题标题】:Android Espresso wait for text to appearAndroid Espresso 等待文本出现
【发布时间】:2018-04-12 12:08:02
【问题描述】:

我正在尝试使用 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());

//Here I need to wait for the text "Hi ..."

一些解释:按下登录按钮后,聊天机器人会说“嗨”并提供更多信息。我想等待最后一条消息出现在屏幕上。

【问题讨论】:

    标签: android chatbot android-espresso


    【解决方案1】:

    我喜欢上面@jeprubio 的回答,但是我遇到了与cmets 中提到的@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 次,然后它抛出一个异常并且测试失败。对什么是“机器人”感到困惑?查看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 测试中处理异步事件的方法,但它通常要求您在应用代码中嵌入测试特定代码(即挂钩)以同步测试。这对我来说似乎很奇怪,并且在一个拥有成熟应用程序和多个开发人员每天提交代码的团队中工作,似乎为了测试而在应用程序中的任何地方改造空闲资源将是很多额外的工作。就个人而言,我更喜欢将应用程序和测试代码尽可能分开。 /结束咆哮

    【讨论】:

    • 绝对令人难以置信的解决方案,以及我在这个网站上看到的一些最好的代码。谢谢!
    • 我发现此解决方案的一个问题是matcher.matches(it) 将返回 true,即使可见性设置为不可见,但单击将失败,因为它不可见。简单的解决方案就是使用matcher.matches(child).and(child.isVisible)
    • 为什么不直接遍历 check(matches(isDisplayed())) ,在视图显示之前捕获 NoMatchingViewException 异常?似乎比这个解决方案简单得多。
    • onView(withId(someId)).check(matches(isDisplayed())) 第一次可能会失败,抛出 NoMatchingViewException 异常,但是如果您继续尝试该检查代码直到视图可见,这会奏效。我在仅在几秒钟后出现的视图上使用此策略,并且一旦视图可见,它就可以正常工作,检查成功。
    • 也许这不适用于您的用例。就我而言,一切都发生在一个包含多个片段的主要活动中。如果我的策略仍然有效,我会在空闲时间尝试多项活动:)
    【解决方案2】:

    您可以创建一个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 毫秒)的超时。

    【讨论】:

    • 谢谢,我会试试的。你有空闲资源使用的工作示例吗?当我在这里搜索时,所有示例都不适合我。
    • 我没有。我很确定这也可以通过空闲资源来完成,但我仍然使用这个waitId 方法。
    • 在我看来,创建idling resource 应该更有效,但waitId 方法使您的代码更易于阅读和遵循。这就是我使用最后一个的原因。
    • @jeprubio 这个 viewAction 很有趣,我想弄清楚如何使用这个动作。上面代码 sn-p 中的yourViewMatcher 到底是什么?谢谢
    • 我已经检查了我的代码并以这种方式使用它:onView(isRoot()).perform(waitId(R.id.theIdToWaitFor, 5000));.
    【解决方案3】:

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

    但是,如果您正在等待在视图层次结构中已经存在的 TextView 中更改文本,那么我强烈建议您定义一个在 TextView 本身上运行的 ViewAction 以进行更好的测试测试失败时输出。

    定义一个在特定TextView 上操作而不是在根视图上操作的ViewAction 是一个如下三步过程。

    首先,定义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))
    

    【讨论】:

    • 这对我来说适用于动态文本,谢谢。在 Kotlin 中,您可以根据需要跳过类创建步骤,只需执行 fun waitForText(text: String, timeout: Long): ViewAction = object : ViewAction { /* implement members */ }
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多