Espresso: Thread.sleep ();

102

Espresso afirma que no es necesario Thread.sleep();, pero mi código no funciona a menos que lo incluya. Me estoy conectando a una IP. Mientras se conecta, se muestra un cuadro de diálogo de progreso. Necesito sleepesperar a que se cierre el diálogo. Este es mi fragmento de prueba donde lo uso:

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

He probado este código con y sin el Thread.sleep();pero dice R.id.Buttonque no existe. La única forma en que puedo hacer que funcione es durmiendo.

Además, he intentado reemplazar Thread.sleep();con cosas como getInstrumentation().waitForIdleSync();y todavía no tengo suerte.

¿Es esta la única forma de hacer esto? ¿O me estoy perdiendo algo?

Gracias por adelantado.

Chad Bingham
fuente
¿Es posible que coloque un bucle while no deseado de todos modos si desea bloquear la llamada?
kedark
ok .. déjame explicarte. 2 sugerencias para usted 1º) Implemente algo como un mecanismo de devolución de llamada. on-connection-set llamar a un método y mostrar la vista. 2º) desea crear el retraso entre IP.enterIP (); y onView (....) para que pueda poner el bucle while que creará el tipo de demora similar para llamar a onview (..) ... pero creo que, si es posible, prefiera la opción No 1. (creando la devolución de llamada mecanismo) ...
kedark
@kedark Sí, esa es una opción, pero ¿es esa la solución de Espresso?
Chad Bingham
Hay comentarios sin respuesta en su pregunta, ¿podría responderlos?
Bolhoso
@Bolhoso, ¿qué pregunta?
Chad Bingham

Respuestas:

110

En mi opinión, el enfoque correcto será:

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

Y luego el patrón de uso será:

// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));
Oleksandr Kucherenko
fuente
3
Gracias Alex, ¿por qué elegiste esta opción en lugar de IdlingResource o AsyncTasks?
Tim Boland
1
Este es un enfoque alternativo, en la mayoría de los casos, Espresso hace el trabajo sin ningún problema y tiene un 'código de espera' especial. De hecho, trato de varias formas diferentes, y creo que esta es la arquitectura / diseño de Espresso que mejor se adapta.
Oleksandr Kucherenko
1
@AlexK ¡esto hizo mi compañero de día!
dawid gdanski
1
para mí, falla para api <= 19, en la línea lanza una nueva PerformException.Builder ()
Prabin Timsina
4
Espero que entienda que es una muestra, puede copiar / pegar y modificar según sus propias necesidades. Es completamente su responsabilidad utilizarlo correctamente en sus propias necesidades comerciales, no en las mías.
Oleksandr Kucherenko
47

Gracias a AlexK por su increíble respuesta. Hay casos en los que es necesario retrasar el código. No está necesariamente esperando la respuesta del servidor, pero podría estar esperando a que se complete la animación. Personalmente, tengo un problema con Espresso idolingResources (creo que estamos escribiendo muchas líneas de código para una cosa simple), así que cambié la forma en que AlexK lo estaba haciendo en el siguiente código:

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

Entonces puede crear una Delayclase y poner este método en ella para acceder a ella fácilmente. Puede usarlo en su clase de prueba de la misma manera:onView(isRoot()).perform(waitFor(5000));

Hesam
fuente
7
el método perform puede incluso simplificarse con una línea como esta: uiController.loopMainThreadForAtLeast (millis);
Yair Kukielka
Genial, no sabía eso: thumbs_up @YairKukielka
Hesam
¡Ay por la espera ocupada!
TWiStErRob
Increíble. Estuve buscando eso durante años. +1 para una solución simple para problemas de espera.
Tobias Reich
Una forma mucho mejor de agregar demora en lugar de usarThread.sleep()
Wahib Ul Haq
23

Me encontré con este hilo cuando buscaba una respuesta a un problema similar en el que estaba esperando una respuesta del servidor y cambiando la visibilidad de los elementos en función de la respuesta.

Si bien la solución anterior definitivamente ayudó, finalmente encontré este excelente ejemplo de chiuki y ahora uso ese enfoque como mi opción cada vez que espero que ocurran acciones durante los períodos inactivos de la aplicación.

He añadido ElapsedTimeIdlingResource () a mi propia clase de servicios públicos, ahora se puede utilizar de manera efectiva que, como alternativa Express adecuada, y ahora uso es agradable y limpio:

// 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);
MattMatt
fuente
Me sale un I/TestRunner: java.lang.NoClassDefFoundError: fr.x.app.y.testtools.ElapsedTimeIdlingResourceerror. Alguna idea. Yo uso Proguard pero con la ofuscación desactivada.
Anthony
Intente agregar una -keepdeclaración para las clases que no se encuentran para asegurarse de que ProGuard no las elimine como innecesarias. Más información aquí: developer.android.com/tools/help/proguard.html#keep-code
MattMatt
Publico una pregunta stackoverflow.com/questions/36859528/… . La clase está en seed.txt y mapping.txt
Anthony
2
Si necesita cambiar las políticas de inactividad, probablemente no esté implementando los recursos inactivos correctamente. A largo plazo, es mucho mejor invertir tiempo en solucionarlo. Este método eventualmente conducirá a pruebas lentas y escamosas. Visite google.github.io/android-testing-support-library/docs/espresso/…
Jose Alcérreca
Tienes toda la razón. Esta respuesta tiene más de un año y, desde entonces, el comportamiento de los recursos inactivos ha mejorado de tal manera que el mismo caso de uso en el que usé el código anterior funciona de inmediato, detectando correctamente el cliente API simulado; ya no usamos el anterior. ElapsedTimeIdlingResource en nuestras pruebas instrumentadas por ese motivo. (Por supuesto, también podría Rx todas las cosas, lo que niega la necesidad de piratear en un período de espera). Dicho esto, la forma en que Google hace las cosas no siempre es la mejor: philosophicalhacker.com/post/… .
MattMatt
18

Creo que es más fácil agregar esta línea:

SystemClock.sleep(1500);

Espera una cantidad determinada de milisegundos (de uptimeMillis) antes de regresar. Similar a sleep (largo), pero no lanza InterruptedException; Los eventos interrupt () se aplazan hasta la siguiente operación interrumpible. No regresa hasta que haya transcurrido al menos el número especificado de milisegundos.

Cabezas
fuente
Expresso es para evitar este sueño codificado que causa pruebas escamosas. si este es el caso, también puedo optar por herramientas de caja negra como appium
Emjey
6

Puedes usar los métodos Barista:

BaristaSleepActions.sleep(2000);

BaristaSleepActions.sleep(2, SECONDS);

Barista es una biblioteca que envuelve Espresso para evitar agregar todo el código que necesita la respuesta aceptada. ¡Y aquí tienes un enlace! https://github.com/SchibstedSpain/Barista

Roc Boronat
fuente
No entiendo la diferencia entre esto y simplemente hacer un sueño de hilo
Pablo Caviglia
Honestamente, no recuerdo en qué video de Google un tipo dijo que deberíamos usar esta forma para dormir en lugar de hacer una común Thread.sleep(). ¡Lo siento! Fue en algunos de los primeros videos que Google hizo sobre Espresso pero no recuerdo cuál ... fue hace algunos años. ¡Lo siento! :·) ¡Oh! ¡Editar! Puse el enlace al video en el PR que abrí hace tres años. ¡Echale un vistazo! github.com/AdevintaSpain/Barista/pull/19
Roc Boronat
5

Esto es similar a esta respuesta, pero usa un tiempo de espera en lugar de intentos y se puede encadenar con otras 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()
}

Uso:

onView(withId(R.id.whatever))
    .waitUntilVisible(5000)
    .perform(click())
Big McLargeHuge
fuente
4

Soy nuevo en la codificación y Espresso, así que aunque sé que la solución buena y razonable es usar el ralentí, todavía no soy lo suficientemente inteligente para hacer eso.

Sin embargo, hasta que tenga más conocimientos, todavía necesito que mis pruebas se ejecuten de alguna manera, así que por ahora estoy usando esta solución sucia que hace varios intentos para encontrar un elemento, se detiene si lo encuentra y, si no, duerme brevemente y comienza de nuevo hasta alcanzar el número máximo de intentos (el mayor número de intentos hasta ahora ha sido de alrededor de 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;
}

Estoy usando esto en todos los métodos que encuentran elementos por ID, texto, padre, etc.

static ViewInteraction findById(int itemId) {
    ViewInteraction element = onView(withId(itemId));
    waitForElementUntilDisplayed(element);
    return element;
}
anna3101
fuente
en su ejemplo, el findById(int itemId)método devolverá un elemento (que podría ser NULL) ya sea que waitForElementUntilDisplayed(element);devuelva verdadero o falso ... entonces, eso no está bien
mbob
Solo quería intervenir y decir que, en mi opinión, esta es la mejor solución. IdlingResourceLos s no son suficientes para mí debido a la granularidad de la tasa de sondeo de 5 segundos (demasiado grande para mi caso de uso). La respuesta aceptada tampoco me funciona (la explicación de por qué ya está incluida en el extenso feed de comentarios de esa respuesta). ¡Gracias por esto! Tomé tu idea e hice mi propia solución y funciona como un encanto.
oaskamay
Sí, esta es la única solución que a mí también me funcionó, al querer esperar elementos que no están en la actividad actual.
guilhermekrz
3

Espresso está diseñado para evitar llamadas a sleep () en las pruebas. Su prueba no debe abrir un diálogo para ingresar una IP, esa debe ser responsabilidad de la actividad probada.

Por otro lado, su prueba de IU debería:

  • Espere a que aparezca el cuadro de diálogo IP
  • Complete la dirección IP y haga clic en ingresar
  • Espere a que aparezca su botón y haga clic en él

La prueba debería verse así:

// 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 espera a que termine todo lo que está sucediendo tanto en el hilo de la interfaz de usuario como en el grupo AsyncTask antes de ejecutar las pruebas.

Recuerde que sus pruebas no deben hacer nada que sea responsabilidad de su aplicación. Debe comportarse como un "usuario bien informado": un usuario que hace clic, verifica que se muestra algo en la pantalla, pero, de hecho, conoce los ID de los componentes.

Bolhoso
fuente
2
Su código de ejemplo es esencialmente el mismo código que escribí en mi pregunta.
Chad Bingham
@Binghammer lo que quiero decir es que la prueba debe comportarse como se comporta el usuario. Tal vez el punto que me falta es lo que hace su método IP.enterIP (). ¿Puedes editar tu pregunta y aclararla?
Bolhoso
Mis comentarios dicen lo que hace. Es solo un método en espresso que completa el cuadro de diálogo IP. Todo es UI.
Chad Bingham
mm ... ok, tienes razón, mi prueba básicamente hace lo mismo. ¿Haces algo fuera del hilo de la interfaz de usuario o AsyncTasks?
Bolhoso
16
Espresso no funciona como el código y el texto de esta respuesta parecen implicar. Una llamada de verificación en una ViewInteraction no esperará hasta que el Matcher dado tenga éxito, sino que fallará inmediatamente si no se cumple la condición. La forma correcta de hacer esto es usar AsyncTasks, como se menciona en esta respuesta, o, si de alguna manera no es posible, implementar un IdlingResource que notificará al UiController de Espresso cuando esté bien continuar con la ejecución de la prueba.
haffax
2

Debe usar Espresso Idling Resource, se sugiere en este CodeLab

Un recurso inactivo representa una operación asincrónica cuyos resultados afectan las operaciones posteriores en una prueba de IU. Al registrar recursos inactivos con Espresso, puede validar estas operaciones asincrónicas de manera más confiable al probar su aplicación.

Ejemplo de una llamada asincrónica del presentador

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

Dependencias

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

Para androidx

androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'

Repo oficial: https://github.com/googlecodelabs/android-testing

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

Gastón Saillén
fuente
0

Si bien creo que es mejor usar Idling Resources para esto ( https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/ ), probablemente podría usar esto como alternativa:

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

y luego llámelo en su código como, por ejemplo:

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

en vez de

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

Esto también le permite agregar tiempos de espera para ver acciones y ver afirmaciones.

Piotr Zawadzki
fuente
Utilice esta línea de código a continuación para tratar cualquier caso de prueba de Test Espresso: SystemClock.sleep (1000); // 1 segundo
Nikunjkumar Kapupara
para mí esto solo funciona cambiando esta línea return new TimedViewInteraction(Espresso.onView(viewMatcher));conreturn new TimedViewInteraction(Espresso.onView(viewMatcher).check(matches(isDisplayed())));
Manuel Schmitzberger
0

Mi utilidad repite la ejecución ejecutable o invocable hasta que pasa sin errores o lanza arrojables después de un tiempo de espera. ¡Funciona perfectamente para pruebas de Espresso!

Suponga que la última interacción de vista (clic en el botón) activa algunos subprocesos en segundo plano (red, base de datos, etc.). Como resultado, debería aparecer una nueva pantalla y queremos verificarla en nuestro próximo paso, pero no sabemos cuándo estará lista para ser probada.

El enfoque recomendado es forzar a su aplicación a enviar mensajes sobre estados de subprocesos a su prueba. A veces podemos usar mecanismos integrados como OkHttp3IdlingResource. En otros casos, debe insertar piezas de código en diferentes lugares de las fuentes de su aplicación (¡debe conocer la lógica de la aplicación!) Solo para probar el soporte. Además, deberíamos apagar todas tus animaciones (aunque es parte de la interfaz de usuario).

El otro enfoque está esperando, por ejemplo, SystemClock.sleep (10000). Pero no sabemos cuánto tiempo esperar e incluso los retrasos prolongados no pueden garantizar el éxito. Por otro lado, su prueba durará mucho.

Mi enfoque es agregar una condición de tiempo para ver la interacción. Por ejemplo, probamos que la nueva pantalla debería aparecer durante 10000 mc (tiempo de espera). Pero no esperamos y lo verificamos tan rápido como queremos (por ejemplo, cada 100 ms). Por supuesto, bloqueamos el hilo de prueba de esa manera, pero por lo general, es justo lo que necesitamos en tales casos.

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

Esta es mi fuente de clase:

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

alexshr
fuente
0

Este es un ayudante que estoy usando en Kotlin para pruebas de Android. En mi caso, estoy usando longOperation para imitar la respuesta del servidor, pero puedes ajustarlo a tu propósito.

@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()
}
Bade
fuente
0

Agregaré mi forma de hacer esto a la mezcla:

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

Llamado así:

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

Puede agregar parámetros como iteraciones máximas, longitud de iteración, etc. a la función suspendUntilSuccess.

Todavía prefiero usar recursos inactivos, pero cuando las pruebas funcionan debido a animaciones lentas en el dispositivo, por ejemplo, utilizo esta función y funciona bien. Por supuesto, puede colgarse hasta 5 segundos antes de fallar, por lo que podría aumentar el tiempo de ejecución de sus pruebas si la acción para tener éxito nunca tiene éxito.

Sean Blahovici
fuente