tutorial - test ui android
Espresso: Thread.sleep(); (9)
Creo que es más fácil agregar esta línea:
SystemClock.sleep(1500);
Espera un número dado de milisegundos (de tiempo de actividad de Milis) antes de regresar. Similar a dormir (largo), pero no lanza InterruptedException; Los eventos de interrupt () se aplazan hasta la siguiente operación de interrupción. No regresa hasta que haya transcurrido al menos el número especificado de milisegundos.
Espresso afirma que no hay necesidad de 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 sleep
para esperar que el diálogo se descarte. 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 intentado este código con y sin Thread.sleep();
pero dice que R.id.Button
no existe. La única forma en que puedo hacer que funcione es con el sueño.
Además, he intentado reemplazar Thread.sleep();
con cosas como getInstrumentation().waitForIdleSync();
y aún sin suerte.
¿Es esta la única manera de hacer esto? ¿O me estoy perdiendo algo?
Gracias por adelantado.
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)));
Espresso está diseñado para evitar llamadas sleep () en las pruebas. Su prueba no debe abrir un cuadro de diálogo para ingresar un IP, que debería ser la responsabilidad de la actividad probada.
Por otro lado, su prueba de UI debe:
- Espere a que aparezca el 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 más o menos 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 todo lo que sucede tanto en el hilo de la interfaz de usuario como en el conjunto de tareas AsyncTask finalice antes de ejecutar sus 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 algo se muestra en la pantalla, pero, de hecho, conoce los ID de los componentes
Gracias a AlexK por su increíble respuesta. Hay casos en los que necesita retrasar el código. No necesariamente está esperando la respuesta del servidor, pero podría estar esperando a que se realice la animación. Personalmente tengo problemas con Espresso idolingResources (creo que estamos escribiendo muchas líneas de código para algo simple) así que cambié la forma en que AlexK estaba haciendo 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 clase Delay
y poner este método en ella para poder acceder a ella fácilmente. Puede usarlo en su clase de prueba de la misma manera: onView(isRoot()).perform(waitFor(5000));
Me encontré con este hilo al buscar una respuesta a un problema similar en el que estaba esperando una respuesta del servidor y cambiar 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 cuando estoy esperando que ocurran acciones durante los períodos de inactividad de la aplicación.
He agregado ElapsedTimeIdlingResource() a mi propia clase de utilidades, ahora puedo usar eso efectivamente como una alternativa adecuada para Espresso, y ahora el uso es bueno 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);
Mi utilidad repite la ejecución ejecutable o invocable hasta que pasa sin errores o arroja arrojable después de un tiempo de espera. ¡Funciona perfectamente para las pruebas de Espresso!
Supongamos que la interacción de la última vista (clic de botón) activa algunos hilos de fondo (red, base de datos, etc.). Como resultado, debería aparecer una nueva pantalla y queremos verificarla en nuestro siguiente paso, pero no sabemos cuándo estará lista para ser probada la nueva pantalla.
El enfoque recomendado es forzar a su aplicación a enviar mensajes sobre estados de hilos a su prueba. Algunas veces podemos usar mecanismos incorporados como OkHttp3IdlingResource. En otros casos, debe insertar fragmentos de código en diferentes lugares de las fuentes de su aplicación (¡debe conocer la lógica de la aplicación!) Para probar solo la compatibilidad. Además, debemos desactivar 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 las largas demoras no pueden garantizar el éxito. Por otro lado, tu 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 revisamos 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
Puedes simplemente usar los métodos de Barista:
BaristaSleepActions.sleep(2000);
BaristaSleepActions.sleep(2, SECONDS);
Barista es una biblioteca que envuelve Espresso para evitar agregar todo el código necesario para la respuesta aceptada. ¡Y aquí hay un enlace! https://github.com/SchibstedSpain/Barista
Si bien creo que es mejor utilizar los recursos de Idling para esto ( google.github.io/android-testing-support-library/docs/espresso/… ), probablemente pueda utilizar esto como una 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ámalo en tu código como, por ejemplo:
onViewWithTimeout(withId(R.id.button).perform(click());
en lugar de
onView(withId(R.id.button).perform(click());
Esto también le permite agregar tiempos de espera para ver acciones y ver aserciones.
Soy nuevo en codificación y Espresso, así que aunque sé que la solución buena y razonable es usar el ralentí, aún no soy lo suficientemente inteligente como para hacerlo.
Hasta que tenga más conocimiento, aún 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 nuevamente hasta que alcance el máximo de intentos (el mayor número de intentos hasta ahora ha sido 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 están buscando elementos por ID, texto, padre, etc.
static ViewInteraction findById(int itemId) {
ViewInteraction element = onView(withId(itemId));
waitForElementUntilDisplayed(element);
return element;
}