Espresso affirme qu'il n'est pas nécessaire d'utiliser Thread.sleep();
, mais mon code ne fonctionne pas à moins que je ne l'inclue. Je me connecte à une adresse IP. Lors de la connexion, une boîte de dialogue de progression apparaît. J'ai besoin d'un sleep
pour attendre que la boîte de dialogue disparaisse. Ceci est mon extrait de test où je l'utilise:
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());
J'ai essayé ce code avec et sans la Thread.sleep();
mais il est écrit que R.id.Button
N'existe pas. Le seul moyen de le faire fonctionner est de dormir.
De plus, j'ai essayé de remplacer Thread.sleep();
par des choses comme getInstrumentation().waitForIdleSync();
et toujours pas de chance.
Est-ce la seule façon de faire cela? Ou est-ce que je manque quelque chose?
Merci d'avance.
Dans mon esprit, une approche correcte sera:
/** 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();
}
};
}
Et puis le modèle d'utilisation sera:
// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));
Merci à AlexK pour sa réponse géniale. Il y a des cas pour lesquels vous devez retarder le code. Il n'attend pas nécessairement la réponse du serveur, mais attend peut-être que l'animation soit terminée. J'ai personnellement un problème avec les ressources idolingResources Espresso (je pense que nous écrivons beaucoup de lignes de code pour une chose simple), alors j'ai changé la façon dont AlexK procédait dans le code suivant:
/**
* 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);
}
};
}
Vous pouvez donc créer une classe Delay
et y insérer cette méthode afin de pouvoir y accéder facilement. Vous pouvez l'utiliser dans votre classe de test de la même manière: onView(isRoot()).perform(waitFor(5000));
Je suis tombé sur ce fil lorsque je cherchais une réponse à un problème similaire: j'attendais une réponse du serveur et changeais la visibilité des éléments en fonction de la réponse.
Bien que la solution ci-dessus ait définitivement aidé, j’ai finalement trouvé cet excellent exemple de chiuki et utilise maintenant cette approche comme point de mire lorsque j’attends que des actions se produisent pendant les périodes d’inactivité des applications.
J'ai ajouté ElapsedTimeIdlingResource () à ma propre classe d'utilitaires, je peux maintenant l'utiliser efficacement comme alternative à Espresso, et maintenant, son utilisation est agréable et propre:
// 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);
Je pense que c'est plus facile d'ajouter cette ligne:
SystemClock.sleep(1500);
Attend un nombre donné de millisecondes (de disponibilité), avant de revenir. Similaire au sommeil (long), mais ne lance pas InterruptedException; Les événements interrupt () sont différés jusqu'à la prochaine opération interruptible. Ne retourne pas avant qu'au moins le nombre de millisecondes spécifié se soit écoulé.
Vous pouvez simplement utiliser les méthodes Barista:
BaristaSleepActions.sleep(2000);
BaristaSleepActions.sleep(2, SECONDS);
Barista est une bibliothèque qui enveloppe Espresso pour éviter d'ajouter tout le code nécessaire à la réponse acceptée. Et voici un lien! https://github.com/SchibstedSpain/Barista
Je suis novice en codage et en espresso, alors bien que je sache que la bonne et raisonnable solution consiste à utiliser la marche au ralenti, je ne suis pas encore assez intelligent pour le faire.
Jusqu'à ce que je devienne plus informé, j'ai toujours besoin de mes tests pour pouvoir fonctionner, donc pour l'instant j'utilise cette solution sale qui fait plusieurs tentatives pour trouver un élément, s'arrête s'il le trouve et sinon, dort et démarre brièvement à nouveau jusqu’à atteindre le maximum de tentatives (le plus grand nombre de tentatives jusqu’à présent a été d’environ 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;
}
J'utilise ceci dans toutes les méthodes qui recherchent des éléments par ID, texte, parent, etc.:
static ViewInteraction findById(int itemId) {
ViewInteraction element = onView(withId(itemId));
waitForElementUntilDisplayed(element);
return element;
}
Espresso est conçu pour éviter les appels sleep () dans les tests. Votre test ne doit pas ouvrir une boîte de dialogue permettant de saisir une adresse IP, ce qui devrait incomber à l’activité testée.
D'autre part, votre test d'interface utilisateur doit:
Le test devrait ressembler à ceci:
// 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 attend que tout ce qui se passe dans le thread d'interface utilisateur et dans le pool AsyncTask soit terminé avant d'exécuter vos tests.
N'oubliez pas que vos tests ne doivent en aucun cas être la responsabilité de votre application. Il devrait se comporter comme un "utilisateur averti": un utilisateur qui clique, vérifie que quelque chose est affiché à l'écran, mais qui connaît en fait les identifiants des composants
Vous devriez utiliser Espresso Idling Resource, il est suggéré à ceci CodeLab
Une ressource inactive représente une opération asynchrone dont les résultats affectent les opérations suivantes dans un test d'interface utilisateur. En enregistrant des ressources inactives avec Espresso, vous pouvez valider ces opérations asynchrones de manière plus fiable lors du test de votre application.
Exemple d'appel asynchrone du présentateur
@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);
}
});
}
Les dépendances
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'androidx.test.espresso:espresso-idling-resource:3.1.1'
Pour androidx
androidTestImplementation 'com.Android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.Android.support.test.espresso:espresso-idling-resource:3.0.2'
Rapport officiel: https://github.com/googlecodelabs/Android-testing
Exemple IdlingResource: https://github.com/googlesamples/Android-testing/tree/master/ui/espresso/IdlingResourceSample =
Ceci est similaire à cette réponse mais utilise un délai d’attente au lieu de tentatives et peut être chaîné avec d’autres 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()
}
Usage:
onView(withId(R.id.whatever))
.waitUntilVisible(5000)
.perform(click())
Ceci est une aide que j'utilise dans Kotlin pour Android Tests. Dans mon cas, j'utilise longOperation pour imiter la réponse du serveur, mais vous pouvez la modifier à votre convenance.
@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()
}
Mon utilitaire répète l'exécution exécutable ou appelable jusqu'à ce qu'il passe sans erreur ou jette jetable après un délai d'attente. Cela fonctionne parfaitement pour les tests d'espresso!
Supposons que la dernière interaction de vue (clic du bouton) active certains threads d'arrière-plan (réseau, base de données, etc.). En conséquence, un nouvel écran devrait apparaître et nous souhaitons le vérifier à l'étape suivante, mais nous ne savons pas quand le nouvel écran sera prêt à être testé.
L'approche recommandée consiste à forcer votre application à envoyer des messages sur les états de threads à votre test. Parfois, nous pouvons utiliser des mécanismes intégrés tels que OkHttp3IdlingResource. Dans d'autres cas, vous devez insérer des fragments de code à différents endroits de vos sources d'application (vous devez connaître la logique de l'application!) Uniquement à des fins de test. De plus, nous devrions désactiver toutes vos animations (bien que ce soit la partie de l'interface utilisateur).
L’autre approche est en attente, par exemple SystemClock.sleep (10000). Mais nous ne savons pas combien de temps attendre et même de longs retards ne peuvent garantir le succès. D'autre part, votre test durera longtemps.
Mon approche consiste à ajouter une condition de temps pour visualiser l'interaction. Par exemple. nous testons que le nouvel écran devrait apparaître pendant 10000 mc (timeout). Mais nous n’attendons pas et le vérifions aussi vite que nous le souhaitons (toutes les 100 ms, par exemple). Bien sûr, nous bloquons le fil de test de cette manière, mais c’est généralement ce dont nous avons besoin dans de tels cas.
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())));
Ceci est ma source de classe:
/**
* 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/ca90212e49e74eb201fbc976255b47e
Bien que je pense qu'il est préférable d'utiliser Idling Resources pour cela ( https://google.github.io/Android-testing-support-library/docs/espresso/idling-resource/ ), vous pouvez probablement utiliser ceci comme une solution de secours:
/**
* 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();
}
}
}
puis appelez-le dans votre code, par exemple:
onViewWithTimeout(withId(R.id.button).perform(click());
au lieu de
onView(withId(R.id.button).perform(click());
Cela vous permet également d'ajouter des délais d'attente pour les actions d'affichage et les assertions d'affichage.
Je vais ajouter ma façon de faire cela au mélange:
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)
}
}
Appelé comme ça:
suspendUntilSuccess({
checkThat.viewIsVisible(R.id.textView)
})
Vous pouvez ajouter des paramètres tels que le nombre maximal d’itérations, la longueur des itérations, etc. à la fonction suspendUntilSuccess.
Je préfère toujours utiliser des ressources inactives, mais lorsque les tests prennent du retard à cause des animations lentes sur l'appareil, par exemple, j'utilise cette fonction et cela fonctionne bien. Bien entendu, il peut s’arrêter pendant 5 secondes, ce qui augmenterait le temps d’exécution de vos tests si l’action réussie ne réussissait jamais.