Espresso:Thread.sleep();

时间:2014-01-28 22:06:55

标签: android testing android-espresso

Espresso声称不需要Thread.sleep();,但我的代码不起作用,除非我包含它。我正在连接到IP。连接时,会显示进度对话框。我需要一个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();之类的内容替换getInstrumentation().waitForIdleSync();但仍然没有运气。

这是唯一的方法吗?或者我错过了什么?

提前致谢。

14 个答案:

答案 0 :(得分:96)

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

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

答案 1 :(得分:40)

感谢AlexK的精彩回答。有些情况下你需要在代码中做一些延迟。它不一定等待服务器响应,但可能正在等待动画完成。我个人对Espresso idolingResources有问题(我认为我们正在为一个简单的事情编写许多代码行)所以我改变了AlexK对以下代码的处理方式:

fade-in

因此,您可以创建一个opacity类并将此方法放入其中,以便轻松访问它。 您可以在Test类中使用它:/** * 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); } }; }

答案 2 :(得分:20)

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

虽然上面的解决方案肯定有帮助,但我最终找到了this excellent example from 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);

答案 3 :(得分:14)

我认为添加此行更容易:

SystemClock.sleep(1500);

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

答案 4 :(得分:7)

您可以使用Barista方法:

BaristaSleepActions.sleep(2000);

BaristaSleepActions.sleep(2, SECONDS);

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

答案 5 :(得分:3)

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

答案 6 :(得分:3)

我是编码和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;
}

答案 7 :(得分:1)

您应该使用Espresso闲置资源,此their docs

建议
  

空闲资源表示异步操作,其结果   影响UI测试中的后续操作。通过注册空闲   使用Espresso获得资源,您可以验证这些异步   在测试您的应用程序时,操作更加可靠。

来自演示者的异步呼叫示例

@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'

官方仓库: CodeLab

空闲资源示例: https://github.com/googlecodelabs/android-testing

答案 8 :(得分:0)

虽然我认为最好使用空闲资源(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());

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

答案 9 :(得分:0)

我的实用程序重复执行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

答案 10 :(得分:0)

这是我在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()
}

答案 11 :(得分:0)

这类似于this answer,但是使用超时代替尝试,并且可以与其他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: NoMatchingViewException) {
            Thread.sleep(50)
        }
    } while (System.currentTimeMillis() < endTime)

    throw TimeoutException()
}

用法:

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

答案 12 :(得分:0)

我将添加此方法的方式添加到混音中

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秒钟,因此,如果成功的操作永远不会成功,它可能会增加测试的执行时间。

答案 13 :(得分:0)

您还可以使用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);

    }