使用Espresso的Thread.sleep()

128

Espresso声称不需要Thread.sleep(),但是如果我没有包含它,我的代码就无法工作。我正在连接到一个IP地址,在连接过程中,显示进度对话框。我需要使用Thread.sleep()来等待对话框消失。这是我使用它的测试代码:

    IP.enterIP(); // fills out an IP dialog (this is done with espresso)

    //progress dialog is now shown
    Thread.sleep(1500);

    onView(withId(R.id.button).perform(click());

我尝试过不使用Thread.sleep()调用这段代码,但它会说R.id.Button不存在。唯一我能让它正常工作的方法就是使用Thread.sleep()

此外,我尝试将Thread.sleep()替换为像getInstrumentation().waitForIdleSync()之类的东西,但仍然没有成功。

这是唯一的方法吗?还是我漏了什么?


你能否将不需要的 While 循环放在任何你想要的阻塞调用中? - kedark
好的,让我解释一下。针对您的两个建议: 第一, 实现类似"回调"的机制,即在连接建立时调用一个方法并显示视图。第二,您要在IP.enterIP();和onView(....)之间创建延迟时间,因此可以放置while循环来创建类似的延迟以调用onview(..)。但我认为如果可能的话,请优先考虑选项1(创建回调机制)。 - kedark
你的问题中有未回复的评论,你可以回答一下吗? - Bolhoso
@Bolhoso,什么问题? - Chad Bingham
我有许多线程和异步任务。 - Chad Bingham
显示剩余6条评论
14个回答

120

在我看来,正确的方法是:

/** Perform action of waiting for a specific view id. */
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();
        }
    };
}

然后使用模式将是:

// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));

4
谢谢,Alex。为什么你选择了这个选项而不是IdlingResource或者AsyncTasks? - Tim Boland
1
这是一种解决方法,大多数情况下,Espresso在没有任何问题和特殊的“等待代码”的情况下完成工作。我实际上尝试了几种不同的方法,并认为这种方法最符合Espresso的架构/设计。 - Oleksandr Kucherenko
1
@AlexK,这让我感到非常开心,伙计! - dawid gdanski
6
希望您能理解,这只是一个样本,您可以复制/粘贴并根据自己的需要进行修改。在您自己的业务需求中适当使用它完全是您的责任,而不是我的责任。 - Oleksandr Kucherenko
1
@AlexK:然而,你正在搜索其根元素,但如果你想搜索一个在根元素中的元素,但是根元素在下一个屏幕(活动)中,你的元素将不再被找到。例如:假设我们有一个LoginActivity->输入凭据并点击登录按钮。我想等待下一个活动(HomeActivity)显示。在这种情况下,你方法中的isRoot只会知道LoginActivity。 - mbob
显示剩余12条评论

61

感谢AlexK的精彩答案。在某些情况下,您需要延迟代码。这不一定是等待服务器响应,而可能是等待动画完成。我个人在使用Espresso idolingResources时遇到问题(我认为我们为简单的事情编写了很多行代码),所以我将AlexK的方法改为以下代码:

/**
 * Perform action of waiting for a specific time.
 */
public static ViewAction waitFor(final long millis) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isRoot();
        }

        @Override
        public String getDescription() {
            return "Wait for " + millis + " milliseconds.";
        }

        @Override
        public void perform(UiController uiController, final View view) {
            uiController.loopMainThreadForAtLeast(millis);
        }
    };
}

所以您可以创建一个Delay类,并将此方法放入其中,以便轻松访问它。 您可以在Test类中以相同的方式使用它:onView(isRoot())。perform(waitFor(5000));


7
perform方法甚至可以简化为一行,像这样:uiController.loopMainThreadForAtLeast(millis); - Yair Kukielka
太棒了,我不知道那个 :thumbs_up @YairKukielka - Hesam
忙等待真是太糟糕了。 - TWiStErRob
太棒了。我找了好久才找到这个。对于等待问题的简单解决方案加1。 - Tobias Reich
2
我不明白为什么称之为 ViewAction,与调用 SystemClock.sleep(millis) 没有什么区别。两者都会在等待固定的毫秒数后返回。我强烈建议定义你的 ViewAction 类来等待特定条件(如此处所示 https://dev59.com/xGEi5IYBdhLWcg3wRauf#22563297 和 https://dev59.com/JFUL5IYBdhLWcg3wyKok#64497518),这样它们在大多数情况下可以更快地返回,并且只在错误情况下等待最长时间。 - Adil Hussain
显示剩余2条评论

25

当我在寻找解决类似问题的答案时,我偶然发现了这个帖子。我的问题是我在等待服务器响应并根据响应更改元素的可见性。

虽然上面的解决方案确实有所帮助,但我最终发现了chiuki的这个优秀的示例,现在我会在应用程序空闲期间等待动作发生时使用该方法。

我已经将ElapsedTimeIdlingResource()添加到自己的实用程序类中,现在可以有效地将其作为Espresso的替代方式,并且使用非常干净:

// Make sure Espresso does not time out
IdlingPolicies.setMasterPolicyTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);
IdlingPolicies.setIdlingResourceTimeout(waitingTime * 2, TimeUnit.MILLISECONDS);

// Now we wait
IdlingResource idlingResource = new ElapsedTimeIdlingResource(waitingTime);
Espresso.registerIdlingResources(idlingResource);

// Stop and verify
onView(withId(R.id.toggle_button))
    .check(matches(withText(R.string.stop)))
    .perform(click());
onView(withId(R.id.result))
    .check(matches(withText(success ? R.string.success: R.string.failure)));

// Clean up
Espresso.unregisterIdlingResources(idlingResource);

我遇到了一个 I/TestRunner: java.lang.NoClassDefFoundError: fr.x.app.y.testtools.ElapsedTimeIdlingResource 错误。有什么想法吗?我使用了 Proguard,但禁用了混淆。 - Anthony
尝试添加“-keep”语句以保留未找到的类,以确保ProGuard不会将它们作为不必要的内容删除。更多信息请参见:http://developer.android.com/tools/help/proguard.html#keep-code - MattMatt
我在http://stackoverflow.com/questions/36859528/java-lang-noclassdeffounderror-with-espresso-and-proguard上发布了一个问题。该类位于seed.txt和mapping.txt中。 - Anthony
2
如果您需要更改空闲策略,那么您可能没有正确实现空闲资源。从长远来看,最好投入时间来修复它。这种方法最终会导致测试变慢和不稳定。请查看https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/。 - Jose Alcérreca
1
你说得很对。这个答案已经超过一年了,自那时起,空闲资源的行为已经有所改进,以至于我用上面的代码来进行的相同用例现在能够完美地检测到模拟的 API 客户端 - 我们不再在我们的仪器化测试中使用上述的 ElapsedTimeIdlingResource,因此理由和原因都是合适的。(当然,你也可以 Rx 所有的东西,这样就不需要黑客式地等待了)。尽管如此,谷歌的做法并不总是最好的:http://www.philosophicalhacker.com/post/psa-dont-use-esprsso-idling-resources-like-this/. - MattMatt

19

我认为添加这行更容易:

SystemClock.sleep(1500);

等待给定的毫秒数(以uptimeMillis为单位),然后返回。类似于sleep(long),但不会引发InterruptedException;interrupt()事件将被推迟到下一个可中断操作。直到经过了指定的毫秒数,才会返回。


1
Expresso旨在避免这些导致测试不稳定的硬编码休眠。如果是这种情况,我也可以选择像Appium这样的黑盒工具。 - Emjey
关于Espresso不推荐“休眠”的更多信息:https://developer.android.com/training/testing/espresso/idling-resource#identify-when-needed。虽然如此,我会开始使用它进行测试,然后进行重构。 - JCarlosR
有趣的是,我先尝试了所有困难的解决方案,而这个才是有效的 jajajaja。 - Cesar Lopes

12

这类似于此答案,但使用超时而不是尝试,并且可以与其他的ViewInteractions链接:

/**
 * Wait for view to be visible
 */
fun ViewInteraction.waitUntilVisible(timeout: Long): ViewInteraction {
    val startTime = System.currentTimeMillis()
    val endTime = startTime + timeout

    do {
        try {
            check(matches(isDisplayed()))
            return this
        } catch (e: AssertionFailedError) {
            Thread.sleep(50)
        }
    } while (System.currentTimeMillis() < endTime)

    throw TimeoutException()
}

使用方式:

onView(withId(R.id.whatever))
    .waitUntilVisible(5000)
    .perform(click())

2
我使用了这种方法,但它对我并没有完全起作用。我不得不捕获AssertionFailedError而不是NoMatchingViewException。有了这个改变,它现在完美地工作了。 - moyo

7
您可以使用Barista的方法: BaristaSleepActions.sleep(2000); BaristaSleepActions.sleep(2, SECONDS); Barista是一个库,它包装了Espresso,避免了添加所有需要的代码。这里有一个链接! https://github.com/SchibstedSpain/Barista

2
我不明白这个和仅仅执行线程休眠的区别。 - Pablo Caviglia
1
老实说,我不记得在 Google 的哪个视频中有个人说过我们应该使用这种方式来进行睡眠,而不是使用常规的 Thread.sleep()。抱歉!这是在 Google 最早关于 Espresso 的一些视频中提到的,但我不记得是哪一个了……那是几年前的事了。对不起!:·)哦!编辑!我在三年前提交的 PR 中放了视频链接。看看吧!https://github.com/AdevintaSpain/Barista/pull/19 - Roc Boronat

4

我刚开始学编程和Espresso,所以虽然我知道使用空闲等待的好方法,但我还不够聪明。

在我变得更加熟练之前,我仍然需要让我的测试运行,因此现在我使用这个不太好的解决方案,它会尝试多次查找元素,如果找到了就停止,否则短暂地休眠并重新开始,直到达到最大尝试次数(迄今为止尝试的最高次数约为150次)。

private static boolean waitForElementUntilDisplayed(ViewInteraction element) {
    int i = 0;
    while (i++ < ATTEMPTS) {
        try {
            element.check(matches(isDisplayed()));
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            try {
                Thread.sleep(WAITING_TIME);
            } catch (Exception e1) {
                e.printStackTrace();
            }
        }
    }
    return false;
}

我正在使用这个方法来找到所有的ID、文本、父元素等元素:
static ViewInteraction findById(int itemId) {
    ViewInteraction element = onView(withId(itemId));
    waitForElementUntilDisplayed(element);
    return element;
}

在你的例子中,findById(int itemId) 方法将返回一个元素(可能为NULL),无论 waitForElementUntilDisplayed(element); 返回 true 还是 false... 所以,这样做不好。 - mbob
只是想插一句话,我认为这是最好的解决方案。由于5秒轮询速率粒度对我的使用情况来说太大了,IdlingResource 对我来说不够用。被接受的答案对我也不起作用(为什么已经包含在该答案的长评论中)。感谢您的建议!我采纳了您的想法并制定了自己的解决方案,它非常有效。 - oaskamay
是的,这也是我想要等待不在当前活动中的元素时使用的唯一解决方案。 - guilhermekrz
对我来说,即使使用try-catch块,测试仍然无法正常工作(因为任何异常都会阻止测试结果正确)。对我来说,我将递归方法与Thread Sleep和withFailureHandler结合使用,这样可以很好地解决问题。 - Thiago Neves

3

Espresso旨在避免测试中的sleep()调用。您的测试不应打开对话框以输入IP地址,这应该是被测试的活动的责任。

另一方面,您的UI测试应该:

  • 等待IP对话框出现
  • 填写IP地址并单击enter
  • 等待您的按钮出现并单击它

测试应该像这样:

// type the IP and press OK
onView (withId (R.id.dialog_ip_edit_text))
  .check (matches(isDisplayed()))
  .perform (typeText("IP-TO-BE-TYPED"));

onView (withText (R.string.dialog_ok_button_title))
  .check (matches(isDisplayed()))
  .perform (click());

// now, wait for the button and click it
onView (withId (R.id.button))
  .check (matches(isDisplayed()))
  .perform (click());

Espresso会等待UI线程和AsyncTask池中的所有操作完成后再执行您的测试。

请记住,您的测试不应执行任何应由应用程序负责的操作。它应该像“了解良好的用户”一样行事:用户点击、验证在屏幕上显示了某些内容,但实际上知道组件的ID


5
你的示例代码本质上和我在问题中编写的代码是相同的。 - Chad Bingham
@Binghammer 我的意思是测试应该像用户一样运行。也许我缺少的是你的IP.enterIP()方法的作用。你能否编辑你的问题并澄清一下? - Bolhoso
我的注释说明了它的功能。这只是Espresso中填写IP对话框的方法。这都是UI界面。 - Chad Bingham
嗯,好的,你说得对,我的测试基本上也是这么做的。你是否在UI线程之外或使用AsyncTasks? - Bolhoso
17
浓缩咖啡的工作方式与此答案的代码和文本所暗示的不同。对ViewInteraction进行检查调用不会等待给定的Matcher成功,而是在条件不满足时立即失败。正确的方法是使用AsyncTasks,如本答案中所述,或者如果不可能,则实现一个IdlingResource,该资源将告知Espresso的UiController何时可以继续执行测试。 - haffax
@haffax Espresso不是这样工作的,因为被测试的应用程序没有准备好这样做。只有通过正确实现空闲资源才能得出正确答案。其他方法都是捷径,最终会产生负面影响。 - Jose Alcérreca

2
你应该使用Espresso Idling Resource,它被建议在这个CodeLab中。一个idling resource代表了一个异步操作,其结果会影响到UI测试中后续的操作。通过在Espresso中注册idling resource,您可以更可靠地验证这些异步操作,在测试应用程序时更加稳定。

Presenter中一个异步调用的示例。

@Override
public void loadNotes(boolean forceUpdate) {
   mNotesView.setProgressIndicator(true);
   if (forceUpdate) {
       mNotesRepository.refreshData();
   }

   // The network request might be handled in a different thread so make sure Espresso knows
   // that the app is busy until the response is handled.
   EspressoIdlingResource.increment(); // App is busy until further notice

   mNotesRepository.getNotes(new NotesRepository.LoadNotesCallback() {
       @Override
       public void onNotesLoaded(List<Note> notes) {
           EspressoIdlingResource.decrement(); // Set app as idle.
           mNotesView.setProgressIndicator(false);
           mNotesView.showNotes(notes);
       }
   });
}

依赖项

androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
    implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'

对于AndroidX:
最初的回答
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'

官方仓库: https://github.com/googlecodelabs/android-testing

IdlingResource示例: https://github.com/googlesamples/android-testing/tree/master/ui/espresso/IdlingResourceSample

最初的回答: 本文提供了Google官方的Android测试代码库和一个IdlingResource示例链接。


1

你也可以使用CountDownLatch来阻塞线程,直到你收到来自服务器的响应或超时。

Countdown latch是一种简单而优雅的解决方案,无需使用外部库。它还可以帮助你专注于实际要测试的逻辑,而不是过度设计异步等待或等待响应。

void testServerAPIResponse() {


        Latch latch = new CountDownLatch(1);


        //Do your async job
        Service.doSomething(new Callback() {

            @Override
            public void onResponse(){
                ACTUAL_RESULT = SUCCESS;
                latch.countDown(); // notify the count down latch
                // assertEquals(..
            }

        });

        //Wait for api response async
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        assertEquals(expectedResult, ACTUAL_RESULT);

    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接