Espresso: Thread.sleep ();

102

O Espresso afirma que não há necessidade Thread.sleep();, mas meu código não funciona a menos que eu o inclua. Estou me conectando a um IP. Durante a conexão, uma caixa de diálogo de progresso é exibida. Preciso sleepaguardar o encerramento do diálogo. Este é o meu snippet de teste onde o utilizo:

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

Eu tentei este código com e sem o, Thread.sleep();mas ele diz R.id.Buttonque não existe. A única maneira de fazer funcionar é dormindo.

Além disso, tentei substituir Thread.sleep();por coisas como getInstrumentation().waitForIdleSync();e ainda sem sorte.

Esta é a única maneira de fazer isso? Ou eu estou esquecendo de alguma coisa?

Desde já, obrigado.

Chad Bingham
fonte
É possível colocar o loop While indesejado de qualquer maneira.
kedark
ok .. deixe-me explicar. 2 sugestões para você 1) Implementar algo como mecanismo de retorno de chamada. ao estabelecer a conexão chama um método e mostra a visualização. 2º) você deseja criar o atraso entre IP.enterIP (); e onView (....) para que você possa colocar o loop while que criará o tipo de atraso simillar para chamar onview (..) ... mas acho que se possível, prefira a opção nº 1. (criando o call-back mecanismo) ...
kedark
@kedark Sim, é uma opção, mas essa é a solução do Espresso?
Chad Bingham
Há comentários não respondidos em sua pergunta, você poderia respondê-los?
Bolhoso de
@Bolhoso, que pergunta?
Chad Bingham

Respostas:

110

Em minha opinião, a abordagem correta 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();
        }
    };
}

E então o padrão de uso será:

// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));
Oleksandr Kucherenko
fonte
3
Obrigado Alex, por que você escolheu esta opção em vez de IdlingResource ou AsyncTasks?
Tim Boland,
1
Esta é uma abordagem alternativa, na maioria dos casos o Espresso fazendo o trabalho sem problemas e com um 'código de espera' especial. Na verdade, tento várias maneiras diferentes e acho que essa é a que mais combina com a arquitetura / design do Espresso.
Oleksandr Kucherenko,
1
@AlexK isso fez meu companheiro de dia!
dawid gdanski
1
para mim, falha para api <= 19, na linha lança novo PerformException.Builder ()
Prabin Timsina
4
Espero que você entenda que é uma amostra, você pode copiar / colar e modificar conforme suas necessidades. É totalmente sua responsabilidade usá-lo adequadamente nas necessidades do próprio negócio, não nas minhas.
Oleksandr Kucherenko
47

Obrigado a AlexK por sua resposta incrível. Há casos em que você precisa fazer algum atraso no código. Não está necessariamente esperando pela resposta do servidor, mas pode estar esperando que a animação seja concluída. Pessoalmente, tenho problemas com o Espresso idolingResources (acho que estamos escrevendo muitas linhas de código para uma coisa simples), então mudei a maneira como AlexK estava fazendo para o seguinte 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);
        }
    };
}

Então você pode criar uma Delayclasse e colocar este método nela para acessá-la facilmente. Você pode usá-lo em sua classe de teste da mesma maneira:onView(isRoot()).perform(waitFor(5000));

Hesam
fonte
7
o método perform pode até ser simplificado com uma linha como esta: uiController.loopMainThreadForAtLeast (millis);
Yair Kukielka
Incrível, eu não sabia disso: thumbs_up @YairKukielka
Hesam
Caramba para a espera ocupada.
TWiStErRob
Impressionante. Eu estava procurando por isso há muito tempo. 1 para uma solução simples para problemas de espera.
Tobias Reich
Uma maneira muito melhor de adicionar atraso em vez de usarThread.sleep()
Wahib Ul Haq
23

Tropecei neste tópico ao procurar uma resposta para um problema semelhante em que estava esperando uma resposta do servidor e alterando a visibilidade dos elementos com base na resposta.

Embora a solução acima tenha definitivamente ajudado, acabei encontrando este excelente exemplo de chiuki e agora uso essa abordagem como minha opção sempre que estou esperando que as ações ocorram durante os períodos de inatividade do aplicativo.

Eu adicionei ElapsedTimeIdlingResource () à minha própria classe de utilitários, agora posso usar isso efetivamente como uma alternativa adequada ao Espresso e agora o uso é bom e limpo:

// 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
fonte
Recebo um I/TestRunner: java.lang.NoClassDefFoundError: fr.x.app.y.testtools.ElapsedTimeIdlingResourceerro. Qualquer ideia. Eu uso o Proguard, mas com a ofuscação desabilitada.
Anthony
Tente adicionar uma -keepinstrução para classes que não estão sendo encontradas para ter certeza de que o ProGuard não as está removendo como desnecessárias. Mais informações aqui: developer.android.com/tools/help/proguard.html#keep-code
MattMatt
Eu posto uma pergunta stackoverflow.com/questions/36859528/… . A classe está no seed.txt e no mapping.txt
Anthony
2
Se você precisar alterar as políticas de inatividade, provavelmente não está implementando os recursos de inatividade corretamente. No longo prazo, é muito melhor investir tempo para consertar isso. Esse método acabará resultando em testes lentos e fragmentados. Confira google.github.io/android-testing-support-library/docs/espresso/…
Jose Alcérreca
Você está certo. Esta resposta tem mais de um ano e, desde então, o comportamento dos recursos ociosos melhorou de forma que o mesmo caso de uso no qual usei o código acima agora funciona imediatamente, detectando corretamente o cliente de API simulado - não usamos mais o acima ElapsedTimeIdlingResource em nossos testes instrumentados por esse motivo. (Você também pode, claro, Rx todas as coisas, o que nega a necessidade de hackear em um período de espera). Dito isso, a maneira do Google de fazer as coisas nem sempre é a melhor: philosophicalhacker.com/post/… .
MattMatt
18

Acho que é mais fácil adicionar esta linha:

SystemClock.sleep(1500);

Espera um determinado número de milissegundos (de uptimeMillis) antes de retornar. Semelhante a sleep (long), mas não lança InterruptedException; Os eventos interrupt () são adiados até a próxima operação interruptível. Não retorna até que pelo menos o número especificado de milissegundos tenha decorrido.

Cabezas
fonte
O Expresso é para evitar esse sono codificado que causa testes fragmentados. se este for o caso, posso também usar ferramentas de caixa preta como appium
Emjey
6

Você pode apenas usar os métodos do Barista:

BaristaSleepActions.sleep(2000);

BaristaSleepActions.sleep(2, SECONDS);

Barista é uma biblioteca que envolve o Espresso para evitar adicionar todo o código necessário para a resposta aceita. E aqui está um link! https://github.com/SchibstedSpain/Barista

Roc Boronat
fonte
Eu não entendo a diferença entre isso e apenas fazer um thread sleep
Pablo Caviglia
Sinceramente, não me lembro em qual vídeo do Google um cara disse que devíamos usar assim para dormir em vez de fazer um comum Thread.sleep(). Desculpe! Estava em alguns dos primeiros vídeos que o Google fez sobre o Espresso, mas não me lembro qual ... foi há alguns anos. Desculpe! : ·) Oh! Editar! Coloquei o link do vídeo no PR que abri há três anos. Confira! github.com/AdevintaSpain/Barista/pull/19
Roc Boronat
5

Isso é semelhante a esta resposta, mas usa um tempo limite em vez de tentativas e pode ser encadeado com outras 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
fonte
4

Eu sou novo em programação e Espresso, então, embora eu saiba que a solução boa e razoável é usar ociosidade, ainda não sou inteligente o suficiente para fazer isso.

Até que eu me torne mais experiente, ainda preciso que meus testes sejam executados de alguma forma, então, por enquanto, estou usando esta solução suja que faz uma série de tentativas de encontrar um elemento, pára se o encontrar e se não, dorme brevemente e inicia novamente até atingir o número máximo de tentativas (o maior número de tentativas até agora foi em torno 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;
}

Estou usando isso em todos os métodos que encontram elementos por ID, texto, pai etc:

static ViewInteraction findById(int itemId) {
    ViewInteraction element = onView(withId(itemId));
    waitForElementUntilDisplayed(element);
    return element;
}
anna3101
fonte
no seu exemplo, o findById(int itemId)método retornará um elemento (que pode ser NULL) se o waitForElementUntilDisplayed(element);retorna verdadeiro ou falso .... então, isso não está ok
mbob
Só queria intervir e dizer que esta é a melhor solução na minha opinião. IdlingResources não são suficientes para mim devido à granularidade da taxa de pesquisa de 5 segundos (muito grande para o meu caso de uso). A resposta aceita também não funciona para mim (a explicação do motivo já está incluída no feed de comentários longos dessa resposta). Obrigado por isso! Peguei sua ideia e fiz minha própria solução e funcionou perfeitamente.
oaskamay
Sim, esta é a única solução que funcionou para mim também, quando quero esperar por elementos que não estão na atividade atual.
guilhermekrz
3

O Espresso foi desenvolvido para evitar chamadas sleep () nos testes. Seu teste não deve abrir uma caixa de diálogo para inserir um IP, que deve ser de responsabilidade da atividade testada.

Por outro lado, seu teste de IU deve:

  • Aguarde até que a caixa de diálogo IP apareça
  • Preencha o endereço IP e clique em entrar
  • Espere que o seu botão apareça e clique nele

O teste deve ser semelhante a este:

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

O Espresso espera que tudo o que está acontecendo no thread de IU e no pool AsyncTask termine antes de executar seus testes.

Lembre-se de que seus testes não devem fazer nada que seja de responsabilidade do seu aplicativo. Deve se comportar como um "usuário bem informado": um usuário que clica, verifica se algo está mostrado na tela, mas, na verdade, conhece os IDs dos componentes

Bolhoso
fonte
2
Seu código de exemplo é essencialmente o mesmo código que escrevi na minha pergunta.
Chad Bingham
@Binghammer, o que quero dizer é que o teste deve se comportar como o usuário se comporta. Talvez o ponto que estou perdendo seja o que seu método IP.enterIP () faz. Você pode editar sua pergunta e esclarecê-la?
Bolhoso de
Meus comentários dizem o que isso faz. É apenas um método no espresso que preenche a caixa de diálogo IP. É tudo interface do usuário.
Chad Bingham
mm ... ok, então você está certo, meu teste está basicamente fazendo o mesmo. Você faz algo fora do thread de interface do usuário ou AsyncTasks?
Bolhoso
16
O Espresso não funciona como o código e o texto desta resposta parecem sugerir. Uma chamada de verificação em um ViewInteraction não esperará até que o Matcher fornecido seja bem-sucedido, mas falhará imediatamente se a condição não for atendida. A maneira certa de fazer isso é usar AsyncTasks, conforme mencionado nesta resposta, ou, se de alguma forma não for possível, implementar um IdlingResource que notificará o UiController do Espresso quando estiver OK para prosseguir com a execução do teste.
haffax,
2

Você deve usar o Espresso Idling Resource, é sugerido neste CodeLab

Um recurso ocioso representa uma operação assíncrona cujos resultados afetam as operações subsequentes em um teste de IU. Ao registrar recursos inativos com o Espresso, você pode validar essas operações assíncronas de forma mais confiável ao testar seu aplicativo.

Exemplo de uma chamada assíncrona do apresentador

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

Dependências

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

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

Gastón Saillén
fonte
0

Embora eu ache que é melhor usar recursos de inatividade para isso ( https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/ ), você provavelmente poderia usar isso como um substituto:

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

e chame-o em seu código como, por exemplo:

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

ao invés de

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

Isso também permite que você adicione tempos limite para ver ações e ver asserções.

Piotr Zawadzki
fonte
Use a única linha de código abaixo para qualquer caso de teste do Test Espresso: SystemClock.sleep (1000); // 1 segundo
Nikunjkumar Kapupara
para mim, isso só funciona mudando essa linha return new TimedViewInteraction(Espresso.onView(viewMatcher));comreturn new TimedViewInteraction(Espresso.onView(viewMatcher).check(matches(isDisplayed())));
Manuel Schmitzberger
0

Meu utilitário repete a execução executável ou chamável até que passe sem erros ou lance lançável após um tempo limite. Funciona perfeitamente para testes do Espresso!

Suponha que a última interação de visualização (clique de botão) ative alguns threads de fundo (rede, banco de dados etc.). Como resultado, uma nova tela deve aparecer e queremos verificá-la em nossa próxima etapa, mas não sabemos quando a nova tela estará pronta para ser testada.

A abordagem recomendada é forçar seu aplicativo a enviar mensagens sobre estados de threads para o seu teste. Às vezes, podemos usar mecanismos integrados como OkHttp3IdlingResource. Em outros casos, você deve inserir partes de código em locais diferentes das fontes do seu aplicativo (você deve conhecer a lógica do aplicativo!) Apenas para suporte de teste. Além disso, devemos desligar todas as suas animações (embora seja parte da IU).

A outra abordagem é esperar, por exemplo, SystemClock.sleep (10000). Mas não sabemos quanto tempo esperar e mesmo longos atrasos não podem garantir o sucesso. Por outro lado, seu teste vai durar muito.

Minha abordagem é adicionar condição de tempo para visualizar a interação. Por exemplo, testamos que a nova tela deve aparecer durante 10000 mc (tempo limite). Mas não esperamos e verificamos tão rapidamente quanto queremos (por exemplo, a cada 100 ms). Claro, bloqueamos o thread de teste dessa forma, mas geralmente é exatamente o que precisamos em tais 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 é a fonte da minha turma:

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

Este é um auxiliar que estou usando no Kotlin para testes do Android. No meu caso, estou usando longOperation para simular a resposta do servidor, mas você pode ajustá-la para seu 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
fonte
0

Vou adicionar minha maneira de fazer isso à mistura:

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

Chamado assim:

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

Você pode adicionar parâmetros como iterações máximas, comprimento da iteração etc. à função suspendUntilSuccess.

Ainda prefiro usar recursos inativos, mas quando os testes estão dando errado devido a animações lentas do dispositivo por exemplo, eu uso esta função e funciona bem. É claro que ele pode travar por até 5 segundos como está antes de falhar, portanto, pode aumentar o tempo de execução de seus testes se a ação nunca for bem-sucedida.

Sean Blahovici
fonte