【问题标题】:Espresso: Thread.sleep( )浓缩咖啡:Thread.sleep()
【发布时间】:2014-01-28 22:06:54
【问题描述】:

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 循环,因为您想要阻止呼叫。
  • 好的..让我解释一下。给你的 2 条建议 1)实施类似回调的机制。 on-connection-establish 调用一种方法并显示视图。 2nd)你想在 IP.enterIP() 之间创建延迟;和 onView(..) 这样你就可以放置 while 循环,这会产生类似的延迟来调用 onview(..) ...但我觉得如果可能的话,请更喜欢选项 1.(创建回调机制)...
  • @kedark 是的,这是一个选项,但这是 Espresso 的解决方案吗?
  • 你的问题中有没有答案的cmets,你能回答吗?
  • @Bolhoso,什么问题?

标签: android testing android-espresso


【解决方案1】:

我认为正确的方法是:

/** 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)));

【讨论】:

  • 谢谢 Alex,您为什么选择这个选项而不是 IdlingResource 或 AsyncTasks?
  • 这是一种解决方法,在大多数情况下,Espresso 完成这项工作没有任何问题和特殊的“等待代码”。我实际上尝试了几种不同的方式,并认为这是最匹配 Espresso 架构/设计的一种。
  • @AlexK 这让我成为了我的好朋友!
  • 对我来说,它对于 api
  • 我希望您理解它是一个示例,您可以根据自己的需要复制/粘贴和修改。在自己的业务需求中正确使用它完全是您的责任,而不是我的。
【解决方案2】:

感谢 AlexK 的精彩回答。在某些情况下,您需要在代码中进行一些延迟。它不一定要等待服务器响应,但可能正在等待动画完成。我个人对 Espresso 的偶像资源有问题(我认为我们为一个简单的事情编写了很多行代码)所以我将 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 类并将此方法放入其中以便轻松访问它。 你可以在你的测试类中以同样的方式使用它:onView(isRoot()).perform(waitFor(5000));

【讨论】:

  • perform 方法甚至可以简化为这样一行:uiController.loopMainThreadForAtLeast(millis);
  • 太棒了,我不知道 :thumbs_up @YairKukielka
  • 为忙碌的等待干杯。
  • 太棒了。我一直在寻找那个。 +1 用于等待问题的简单解决方案。
  • 我不明白调用此ViewAction 与调用SystemClock.sleep(millis) 有何不同。两者都在返回之前等待固定的毫秒数。我强烈建议您定义 ViewAction 类以等待特定条件(如 herehere 所示),以便它们在大多数情况下返回更快,并且在出错的情况下只等待最大毫秒数。
【解决方案3】:

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

虽然上述解决方案确实有帮助,但我最终找到了this excellent example from chiuki,现在每当我在应用空闲期间等待操作发生时,我都会使用该方法作为我的首选。

我已将ElapsedTimeIdlingResource() 添加到我自己的实用程序类中,现在可以有效地将其用作 Espresso-proper 的替代品,现在使用起来又好又干净:

// 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.ElapsedTimeIdlingResourceerror。任何想法。我使用 Proguard 但禁用混淆。
  • 尝试为未找到的类添加 -keep 语句,以确保 ProGuard 不会将它们删除为不必要的。更多信息在这里:developer.android.com/tools/help/proguard.html#keep-code
  • 我发布了一个问题stackoverflow.com/questions/36859528/…。该类在seed.txt和mapping.txt中
  • 如果您需要更改空闲策略,您可能没有正确实现空闲资源。从长远来看,最好花时间解决这个问题。这种方法最终会导致缓慢而不稳定的测试。查看google.github.io/android-testing-support-library/docs/espresso/…
  • 你说的很对。这个答案已经有一年多了,从那时起,空闲资源的行为得到了改善,以至于我现在使用上述代码的相同用例可以开箱即用,正确检测模拟的 API 客户端——我们不再使用上面的出于这个原因,我们的仪器测试中的 ElapsedTimeIdlingResource。 (你当然也可以接收所有的东西,这样就不需要在等待期间破解)。也就是说,Google 的做事方式并不总是最好的:philosophicalhacker.com/post/…
【解决方案4】:

我认为添加这一行更容易:

SystemClock.sleep(1500);

在返回之前等待给定的毫秒数(uptimeMillis)。类似于 sleep(long),但不抛出 InterruptedException; interrupt() 事件被推迟到下一个可中断操作。至少经过指定的毫秒数后才返回。

【讨论】:

  • Expresso 是为了避免这些导致不稳定测试的硬编码睡眠。如果是这种情况,我也可以使用像 appium 这样的黑盒工具
  • 更多关于 Espresso 不推荐“睡眠”的信息:developer.android.com/training/testing/espresso/… 不过我会开始测试它,然后重构。
【解决方案5】:

这类似于this answer,但使用超时而不是尝试,并且可以与其他 ViewInteraction 链接:

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

【讨论】:

  • 我使用了这种方法,但它并不完全适合我。我不得不捕获 AssertionFailedError 而不是 NoMatchingViewException。有了这个改变,它就完美地工作了
【解决方案6】:

您可以只使用 Barista 方法:

BaristaSleepActions.sleep(2000);

BaristaSleepActions.sleep(2, SECONDS);

Barista 是一个包装 Espresso 的库,以避免添加已接受答案所需的所有代码。这是一个链接! https://github.com/SchibstedSpain/Barista

【讨论】:

  • 我不明白这和只是做一个线程睡眠之间的区别
  • 老实说,我不记得在 Google 的哪个视频中有人说我们应该用这种方式睡觉,而不是普通的Thread.sleep()。对不起!它出现在谷歌制作的关于 Espresso 的第一批视频中,但我不记得是哪一个……那是几年前的事了。对不起! :·) 哦!编辑!我在三年前打开的 PR 中放了视频的链接。看看这个! github.com/AdevintaSpain/Barista/pull/19
【解决方案7】:

我是编码和 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....所以,这不行
  • 只是想插话说这是我认为的最佳解决方案。 IdlingResources 对我来说还不够,因为 5 秒的轮询速率粒度(对于我的用例来说太大了)。接受的答案对我也不起作用(解释为什么已经包含在该答案的长评论提要中)。谢谢你!我采纳了您的想法并制定了自己的解决方案,它就像一个魅力。
  • 是的,当我想等待不在当前活动中的元素时,这也是唯一对我有用的解决方案。
  • 对我来说不起作用,即使使用 try-catch 块测试也显示失败(因为任何异常,都会阻止测试结果正常)。对我来说,我将递归方法与 Thread Sleep 和 withFailureHandler 结合起来,效果很好。
【解决方案8】:

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

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

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

测试应该如下所示:

// 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

【讨论】:

  • 您的示例代码与我在问题中编写的代码基本相同。
  • @Binghammer 我的意思是测试的行为应该像用户行为一样。也许我缺少的一点是您的 IP.enterIP() 方法的作用。你能编辑你的问题并澄清一下吗?
  • 我的 cmets 说它的作用。它只是 espresso 中填写 IP 对话框的一种方法。都是 UI。
  • mm... 好的,所以你是对的,我的测试基本上是这样做的。你是否在 UI 线程或 AsyncTasks 之外做一些事情?
  • Espresso 不像这个答案的代码和文本似乎暗示的那样工作。 ViewInteraction 上的检查调用不会等到给定的 Matcher 成功,而是在不满足条件时立即失败。正确的方法是使用 AsyncTasks,如本答案中所述,或者,如果无法实现,则实现一个 IdlingResource,它会在可以继续执行测试时通知 Espresso 的 UiController。
【解决方案9】:

你应该使用 Espresso Idling Resource,建议在 CodeLab

一个空闲的资源代表一个异步操作,它的结果 影响 UI 测试中的后续操作。通过注册空闲 使用 Espresso 的资源,您可以验证这些异步 在测试您的应用时更可靠地运行。

来自 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

【讨论】:

    【解决方案10】:

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

    倒计时闩锁是一种简单而优雅的解决方案,无需外部库。它还有助于您专注于要测试的实际逻辑,而不是过度设计异步等待或等待响应

    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);
    
        }
    

    【讨论】:

      【解决方案11】:

      虽然我认为最好为此使用空闲资源 (https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/),但您可能可以将其用作后备:

      /**
       * Contains view interactions, view actions and view assertions which allow to set a timeout
       * for finding a view and performing an action/view assertion on it.
       * To be used instead of {@link Espresso}'s methods.
       * 
       * @author Piotr Zawadzki
       */
      public class TimeoutEspresso {
      
          private static final int SLEEP_IN_A_LOOP_TIME = 50;
      
          private static final long DEFAULT_TIMEOUT_IN_MILLIS = 10 * 1000L;
      
          /**
           * Use instead of {@link Espresso#onView(Matcher)}
           * @param timeoutInMillis timeout after which an error is thrown
           * @param viewMatcher view matcher to check for view
           * @return view interaction
           */
          public static TimedViewInteraction onViewWithTimeout(long timeoutInMillis, @NonNull final Matcher<View> viewMatcher) {
      
              final long startTime = System.currentTimeMillis();
              final long endTime = startTime + timeoutInMillis;
      
              do {
                  try {
                      return new TimedViewInteraction(Espresso.onView(viewMatcher));
                  } catch (NoMatchingViewException ex) {
                      //ignore
                  }
      
                  SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
              }
              while (System.currentTimeMillis() < endTime);
      
              // timeout happens
              throw new PerformException.Builder()
                      .withCause(new TimeoutException("Timeout occurred when trying to find: " + viewMatcher.toString()))
                      .build();
          }
      
          /**
           * Use instead of {@link Espresso#onView(Matcher)}.
           * Same as {@link #onViewWithTimeout(long, Matcher)} but with the default timeout {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
           * @param viewMatcher view matcher to check for view
           * @return view interaction
           */
          public static TimedViewInteraction onViewWithTimeout(@NonNull final Matcher<View> viewMatcher) {
              return onViewWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewMatcher);
          }
      
          /**
           * A wrapper around {@link ViewInteraction} which allows to set timeouts for view actions and assertions.
           */
          public static class TimedViewInteraction {
      
              private ViewInteraction wrappedViewInteraction;
      
              public TimedViewInteraction(ViewInteraction wrappedViewInteraction) {
                  this.wrappedViewInteraction = wrappedViewInteraction;
              }
      
              /**
               * @see ViewInteraction#perform(ViewAction...)
               */
              public TimedViewInteraction perform(final ViewAction... viewActions) {
                  wrappedViewInteraction.perform(viewActions);
                  return this;
              }
      
              /**
               * {@link ViewInteraction#perform(ViewAction...)} with a timeout of {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
               * @see ViewInteraction#perform(ViewAction...)
               */
              public TimedViewInteraction performWithTimeout(final ViewAction... viewActions) {
                  return performWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewActions);
              }
      
              /**
               * {@link ViewInteraction#perform(ViewAction...)} with a timeout.
               * @see ViewInteraction#perform(ViewAction...)
               */
              public TimedViewInteraction performWithTimeout(long timeoutInMillis, final ViewAction... viewActions) {
                  final long startTime = System.currentTimeMillis();
                  final long endTime = startTime + timeoutInMillis;
      
                  do {
                      try {
                          return perform(viewActions);
                      } catch (RuntimeException ex) {
                          //ignore
                      }
      
                      SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
                  }
                  while (System.currentTimeMillis() < endTime);
      
                  // timeout happens
                  throw new PerformException.Builder()
                          .withCause(new TimeoutException("Timeout occurred when trying to perform view actions: " + viewActions))
                          .build();
              }
      
              /**
               * @see ViewInteraction#withFailureHandler(FailureHandler)
               */
              public TimedViewInteraction withFailureHandler(FailureHandler failureHandler) {
                  wrappedViewInteraction.withFailureHandler(failureHandler);
                  return this;
              }
      
              /**
               * @see ViewInteraction#inRoot(Matcher)
               */
              public TimedViewInteraction inRoot(Matcher<Root> rootMatcher) {
                  wrappedViewInteraction.inRoot(rootMatcher);
                  return this;
              }
      
              /**
               * @see ViewInteraction#check(ViewAssertion)
               */
              public TimedViewInteraction check(final ViewAssertion viewAssert) {
                  wrappedViewInteraction.check(viewAssert);
                  return this;
              }
      
              /**
               * {@link ViewInteraction#check(ViewAssertion)} with a timeout of {@link #DEFAULT_TIMEOUT_IN_MILLIS}.
               * @see ViewInteraction#check(ViewAssertion)
               */
              public TimedViewInteraction checkWithTimeout(final ViewAssertion viewAssert) {
                  return checkWithTimeout(DEFAULT_TIMEOUT_IN_MILLIS, viewAssert);
              }
      
              /**
               * {@link ViewInteraction#check(ViewAssertion)} with a timeout.
               * @see ViewInteraction#check(ViewAssertion)
               */
              public TimedViewInteraction checkWithTimeout(long timeoutInMillis, final ViewAssertion viewAssert) {
                  final long startTime = System.currentTimeMillis();
                  final long endTime = startTime + timeoutInMillis;
      
                  do {
                      try {
                          return check(viewAssert);
                      } catch (RuntimeException ex) {
                          //ignore
                      }
      
                      SystemClock.sleep(SLEEP_IN_A_LOOP_TIME);
                  }
                  while (System.currentTimeMillis() < endTime);
      
                  // timeout happens
                  throw new PerformException.Builder()
                          .withCause(new TimeoutException("Timeout occurred when trying to check: " + viewAssert.toString()))
                          .build();
              }
          }
      }
      

      然后在您的代码中调用它,例如:

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

      而不是

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

      这还允许您为视图操作和视图断言添加超时。

      【讨论】:

      • 使用下面的单行代码来处理任何 Test Espresso 测试用例:SystemClock.sleep(1000); // 1 秒
      • 对我来说,这只能通过将这一行 return new TimedViewInteraction(Espresso.onView(viewMatcher)); 更改为 return new TimedViewInteraction(Espresso.onView(viewMatcher).check(matches(isDisplayed()))); 来工作
      【解决方案12】:

      我的实用程序重复 runnable 或 callable 执行,直到它通过而没有错误或在超​​时后抛出 throwable。 它非常适合 Espresso 测试!

      假设最后一次视图交互(按钮单击)激活了一些后台线程(网络、数据库等)。 结果,应该会出现一个新屏幕,我们想在下一步中检查它, 但我们不知道新屏幕何时可以进行测试。

      推荐的方法是强制您的应用程序向您的测试发送有关线程状态的消息。 有时我们可以使用 OkHttp3IdlingResource 等内置机制。 在其他情况下,您应该在应用程序源的不同位置插入代码片段(您应该知道应用程序逻辑!)仅用于测试支持。 此外,我们应该关闭所有动画(尽管它是 UI 的一部分)。

      另一种方法是等待,例如SystemClock.sleep(10000)。但是我们不知道要等多久,即使是长时间的延迟也不能保证成功。 另一方面,您的测试将持续很长时间。

      我的方法是添加时间条件来查看交互。例如。我们测试新屏幕应该在 10000 mc(超时)期间出现。 但我们不会等待并尽可能快地检查它(例如每 100 毫秒) 当然,我们以这种方式阻塞测试线程,但通常,这正是我们在这种情况下所需要的。

      Usage:
      
      long timeout=10000;
      long matchDelay=100; //(check every 100 ms)
      EspressoExecutor myExecutor = new EspressoExecutor<ViewInteraction>(timeout, matchDelay);
      
      ViewInteraction loginButton = onView(withId(R.id.login_btn));
      loginButton.perform(click());
      
      myExecutor.callForResult(()->onView(allOf(withId(R.id.title),isDisplayed())));
      

      这是我的课程来源:

      /**
       * Created by alexshr on 02.05.2017.
       */
      
      package com.skb.goodsapp;
      
      import android.os.SystemClock;
      import android.util.Log;
      
      import java.util.Date;
      import java.util.concurrent.Callable;
      
      /**
       * The utility repeats runnable or callable executing until it pass without errors or throws throwable after timeout.
       * It works perfectly for Espresso tests.
       * <p>
       * Suppose the last view interaction (button click) activates some background threads (network, database etc.).
       * As the result new screen should appear and we want to check it in our next step,
       * but we don't know when new screen will be ready to be tested.
       * <p>
       * Recommended approach is to force your app to send messages about threads states to your test.
       * Sometimes we can use built-in mechanisms like OkHttp3IdlingResource.
       * In other cases you should insert code pieces in different places of your app sources (you should known app logic!) for testing support only.
       * Moreover, we should turn off all your animations (although it's the part on ui).
       * <p>
       * The other approach is waiting, e.g. SystemClock.sleep(10000). But we don't known how long to wait and even long delays can't guarantee success.
       * On the other hand your test will last long.
       * <p>
       * My approach is to add time condition to view interaction. E.g. we test that new screen should appear during 10000 mc (timeout).
       * But we don't wait and check new screen as quickly as it appears.
       * Of course, we block test thread such way, but usually it's just what we need in such cases.
       * <p>
       * Usage:
       * <p>
       * long timeout=10000;
       * long matchDelay=100; //(check every 100 ms)
       * EspressoExecutor myExecutor = new EspressoExecutor<ViewInteraction>(timeout, matchDelay);
       * <p>
       * ViewInteraction loginButton = onView(withId(R.id.login_btn));
       * loginButton.perform(click());
       * <p>
       * myExecutor.callForResult(()->onView(allOf(withId(R.id.title),isDisplayed())));
       */
      public class EspressoExecutor<T> {
      
          private static String LOG = EspressoExecutor.class.getSimpleName();
      
          public static long REPEAT_DELAY_DEFAULT = 100;
          public static long BEFORE_DELAY_DEFAULT = 0;
      
          private long mRepeatDelay;//delay between attempts
          private long mBeforeDelay;//to start attempts after this initial delay only
      
          private long mTimeout;//timeout for view interaction
      
          private T mResult;
      
          /**
           * @param timeout     timeout for view interaction
           * @param repeatDelay - delay between executing attempts
           * @param beforeDelay - to start executing attempts after this delay only
           */
      
          public EspressoExecutor(long timeout, long repeatDelay, long beforeDelay) {
              mRepeatDelay = repeatDelay;
              mBeforeDelay = beforeDelay;
              mTimeout = timeout;
              Log.d(LOG, "created timeout=" + timeout + " repeatDelay=" + repeatDelay + " beforeDelay=" + beforeDelay);
          }
      
          public EspressoExecutor(long timeout, long repeatDelay) {
              this(timeout, repeatDelay, BEFORE_DELAY_DEFAULT);
          }
      
          public EspressoExecutor(long timeout) {
              this(timeout, REPEAT_DELAY_DEFAULT);
          }
      
      
          /**
           * call with result
           *
           * @param callable
           * @return callable result
           * or throws RuntimeException (test failure)
           */
          public T call(Callable<T> callable) {
              call(callable, null);
              return mResult;
          }
      
          /**
           * call without result
           *
           * @param runnable
           * @return void
           * or throws RuntimeException (test failure)
           */
          public void call(Runnable runnable) {
              call(runnable, null);
          }
      
          private void call(Object obj, Long initialTime) {
              try {
                  if (initialTime == null) {
                      initialTime = new Date().getTime();
                      Log.d(LOG, "sleep delay= " + mBeforeDelay);
                      SystemClock.sleep(mBeforeDelay);
                  }
      
                  if (obj instanceof Callable) {
                      Log.d(LOG, "call callable");
                      mResult = ((Callable<T>) obj).call();
                  } else {
                      Log.d(LOG, "call runnable");
                      ((Runnable) obj).run();
                  }
              } catch (Throwable e) {
                  long remain = new Date().getTime() - initialTime;
                  Log.d(LOG, "remain time= " + remain);
                  if (remain > mTimeout) {
                      throw new RuntimeException(e);
                  } else {
                      Log.d(LOG, "sleep delay= " + mRepeatDelay);
                      SystemClock.sleep(mRepeatDelay);
                      call(obj, initialTime);
                  }
              }
          }
      }
      

      https://gist.github.com/alexshr/ca90212e49e74eb201fbc976255b47e0

      【讨论】:

        【解决方案13】:

        这是我在 Kotlin 中用于 Android 测试的助手。就我而言,我使用 longOperation 来模拟服务器响应,但您可以根据自己的目的对其进行调整。

        @Test
        fun ensureItemDetailIsCalledForRowClicked() {
            onView(withId(R.id.input_text))
                .perform(ViewActions.typeText(""), ViewActions.closeSoftKeyboard())
            onView(withId(R.id.search_icon)).perform(ViewActions.click())
            longOperation(
                longOperation = { Thread.sleep(1000) },
                callback = {onView(withId(R.id.result_list)).check(isVisible())})
        }
        
        private fun longOperation(
            longOperation: ()-> Unit,
            callback: ()-> Unit
        ){
            Thread{
                longOperation()
                callback()
            }.start()
        }
        

        【讨论】:

          【解决方案14】:

          我将添加我这样做的方式:

          fun suspendUntilSuccess(actionToSucceed: () -> Unit, iteration : Int = 0) {
              try {
                  actionToSucceed.invoke()
              } catch (e: Throwable) {
                  Thread.sleep(200)
                  val incrementedIteration : Int = iteration + 1
                  if (incrementedIteration == 25) {
                      fail("Failed after waiting for action to succeed for 5 seconds.")
                  }
                  suspendUntilSuccess(actionToSucceed, incrementedIteration)
              }
          }
          

          这样称呼:

          suspendUntilSuccess({
              checkThat.viewIsVisible(R.id.textView)
          })
          

          您可以将最大迭代次数、迭代长度等参数添加到suspendUntilSuccess函数。

          我仍然更喜欢使用空闲资源,但是当测试由于设备上的缓慢动画而运行时,我使用此功能并且效果很好。当然,它在失败之前最多可以挂起 5 秒,因此如果要成功的操作永远不会成功,它可能会增加测试的执行时间。

          【讨论】:

            猜你喜欢
            • 2015-10-15
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-09-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多