Espresso: Thread.sleep ();

102

Espresso twierdzi, że nie ma takiej potrzeby Thread.sleep();, ale mój kod nie działa, dopóki go nie dołączę. Łączę się z adresem IP. Podczas łączenia jest wyświetlane okno dialogowe postępu. Muszę sleeppoczekać, aż okno dialogowe zostanie zamknięte. Oto mój fragment testowy, w którym go używam:

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

Próbowałem tego kodu z i bezThread.sleep(); ale mówi R.id.Button, nie istnieje. Jedynym sposobem, żeby to zadziałało, jest sen.

Próbowałem też zastąpić Thread.sleep();takimi rzeczami, jak getInstrumentation().waitForIdleSync();i nadal nie mam szczęścia.

Czy to jedyny sposób, aby to zrobić? A może coś mi brakuje?

Z góry dziękuję.

Chad Bingham
źródło
czy mimo wszystko możesz umieścić niechcianą pętlę While, jeśli chcesz blokować połączenie.
kedark
ok .. pozwól mi wyjaśnić. 2 sugestie dla ciebie 1.) Zastosuj coś w rodzaju mechanizmu oddzwaniania. Przy nawiązaniu połączenia wywołaj jedną metodę i pokaż widok. 2nd) chcesz utworzyć opóźnienie między IP.enterIP (); i onView (....), więc możesz umieścić pętlę while, która stworzy podobny rodzaj opóźnienia wywołania onview (..) ... ale wydaje mi się, że jeśli to możliwe, preferuj opcję nr 1. (tworzenie oddzwaniania mechanizm) ...
kedark
@kedark Tak, to jest opcja, ale czy to rozwiązanie Espresso?
Chad Bingham
Twoje pytanie zawiera komentarze bez odpowiedzi, czy mógłbyś na nie odpowiedzieć?
Bolhoso,
@Bolhoso, jakie pytanie?
Chad Bingham

Odpowiedzi:

111

Moim zdaniem prawidłowe podejście będzie następujące:

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

A następnie wzór użycia będzie:

// wait during 15 seconds for a view
onView(isRoot()).perform(waitId(R.id.dialogEditor, TimeUnit.SECONDS.toMillis(15)));
Oleksandr Kucherenko
źródło
3
Dzięki Alex, dlaczego wybrałeś tę opcję zamiast IdlingResource lub AsyncTasks?
Tim Boland
1
Jest to podejście obejściowe, w większości przypadków Espresso wykonuje pracę bez żadnych problemów i specjalnego „kodu oczekiwania”. Właściwie próbuję na kilka różnych sposobów i myślę, że jest to jeden z najbardziej pasujących do architektury / projektu Espresso.
Oleksandr Kucherenko
1
@AlexK to sprawiło, że mój kolega na dzień!
dawid gdanski
1
dla mnie nie udaje się dla api <= 19, przy rzucie wiersza new PerformException.Builder ()
Prabin Timsina
4
Mam nadzieję, że rozumiesz, że to próbka, którą możesz skopiować / wkleić i zmodyfikować na własne potrzeby. Odpowiedzialność za właściwe wykorzystanie go do własnych potrzeb biznesowych, a nie moich, spoczywa na Tobie.
Oleksandr Kucherenko
48

Dzięki AlexK za jego niesamowitą odpowiedź. Są przypadki, w których trzeba opóźnić kod. Niekoniecznie czeka na odpowiedź serwera, ale może czekać na zakończenie animacji. Osobiście mam problem z idolingResources w Espresso (myślę, że piszemy wiele linii kodu dla prostej rzeczy), więc zmieniłem sposób, w jaki AlexK robił, na następujący kod:

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

Możesz więc utworzyć Delayklasę i umieścić w niej tę metodę, aby mieć do niej łatwy dostęp. Możesz go użyć w swojej klasie testowej w ten sam sposób:onView(isRoot()).perform(waitFor(5000));

Hesam
źródło
7
metodę perform można nawet uprościć za pomocą jednej linii, na przykład: uiController.loopMainThreadForAtLeast (millis);
Yair Kukielka
Super, nie wiedziałem, że: thumbs_up @YairKukielka
Hesam
Ekscytujące zajęte oczekiwanie.
TWiStErRob
Niesamowite. Szukałem tego od wieków. +1 za proste rozwiązanie problemów z oczekiwaniem.
Tobias Reich
O wiele lepszy sposób na dodanie opóźnienia zamiast używaniaThread.sleep()
Wahib Ul Haq
23

Natknąłem się na ten wątek, szukając odpowiedzi na podobny problem, gdzie czekałem na odpowiedź serwera i zmianę widoczności elementów na podstawie odpowiedzi.

Chociaż powyższe rozwiązanie zdecydowanie pomogło, w końcu znalazłem ten doskonały przykład z chiuki i teraz używam tego podejścia jako mojego rozwiązania, gdy czekam na wykonanie akcji w okresach bezczynności aplikacji.

Dodałem ElapsedTimeIdlingResource () do mojej własnej klasy narzędzi, mogę teraz skutecznie używać tego jako odpowiedniej alternatywy dla Espresso, a teraz użycie jest ładne i czyste:

// 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
źródło
Pojawia się I/TestRunner: java.lang.NoClassDefFoundError: fr.x.app.y.testtools.ElapsedTimeIdlingResourcebłąd. Dowolny pomysł. Używam Proguarda, ale z wyłączonym zaciemnianiem.
Anthony
Spróbuj dodać -keepinstrukcję dla klas, które nie zostały znalezione, aby upewnić się, że ProGuard nie usuwa ich jako niepotrzebnych. Więcej informacji tutaj: developer.android.com/tools/help/proguard.html#keep-code
MattMatt
Wysyłam pytanie stackoverflow.com/questions/36859528/… . Klasa znajduje się w seed.txt i mapping.txt
Anthony
2
Jeśli musisz zmienić zasady bezczynności, prawdopodobnie nie implementujesz poprawnie zasobów bezczynnych. Na dłuższą metę znacznie lepiej jest zainwestować czas w naprawienie tego. Ta metoda ostatecznie doprowadzi do powolnych i niestabilnych testów. Sprawdź google.github.io/android-testing-support-library/docs/espresso/…
Jose Alcérreca
Masz w zasadzie rację. Ta odpowiedź ma już ponad rok i od tego czasu zachowanie zasobów bezczynnych poprawiło się tak, że ten sam przypadek użycia, w którym użyłem powyższego kodu, działa teraz od razu, poprawnie wykrywając fałszywego klienta API - nie używamy już powyższego Z tego powodu ElapsedTimeIdlingResource w naszych instrumentalnych testach. (Możesz też oczywiście Rx wszystkie rzeczy, co wyklucza potrzebę hakowania w okresie oczekiwania). To powiedziawszy, sposób działania Google nie zawsze jest najlepszy: Philosaficalhacker.com/post/ ...
MattMatt
18

Myślę, że łatwiej jest dodać tę linię:

SystemClock.sleep(1500);

Czeka określoną liczbę milisekund (uptimeMillis) przed zwróceniem. Podobny do sleep (long), ale nie zgłasza InterruptedException; zdarzenia przerwania () są odroczone do następnej przerywanej operacji. Nie zwraca, dopóki nie upłynie co najmniej określona liczba milisekund.

Cabezas
źródło
Expresso ma unikać tego zakodowanego snu, który powoduje niestabilne testy. jeśli tak jest, mogę równie dobrze skorzystać z narzędzi czarnej skrzynki, takich jak appium
Emjey
6

Możesz po prostu użyć metod Barista:

BaristaSleepActions.sleep(2000);

BaristaSleepActions.sleep(2, SECONDS);

Barista to biblioteka, która opakowuje Espresso, aby uniknąć dodawania całego kodu wymaganego przez zaakceptowaną odpowiedź. A oto link! https://github.com/SchibstedSpain/Barista

Roc Boronat
źródło
Nie rozumiem różnicy między tym a
zwykłym
Szczerze mówiąc, nie pamiętam, w którym filmie z Google facet powiedział, że powinniśmy w ten sposób pospać zamiast robić wspólne Thread.sleep(). Przepraszam! Było to w niektórych z pierwszych filmów, które Google zrobił o Espresso, ale nie pamiętam, który z nich ... to było kilka lat temu. Przepraszam! : ·) Och! Edytować! Link do wideo umieściłem w PR, które otworzyłem trzy lata temu. Sprawdź to! github.com/AdevintaSpain/Barista/pull/19
Roc Boronat
5

Jest to podobne do tej odpowiedzi, ale używa limitu czasu zamiast prób i może być połączone z innymi 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()
}

Stosowanie:

onView(withId(R.id.whatever))
    .waitUntilVisible(5000)
    .perform(click())
Big McLargeHuge
źródło
4

Jestem nowy w programowaniu i espresso, więc chociaż wiem, że dobrym i rozsądnym rozwiązaniem jest użycie biegu jałowego, nie jestem jeszcze wystarczająco inteligentny, aby to zrobić.

Dopóki nie zdobędę większej wiedzy, wciąż potrzebuję moich testów, aby jakoś je uruchomić, więc na razie używam tego brudnego rozwiązania, które podejmuje szereg prób znalezienia elementu, zatrzymuje się, jeśli go znajdzie, a jeśli nie, zasypia i uruchamia się ponownie, aż osiągnie maksymalną liczbę prób (jak dotąd największa liczba prób wynosiła około 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;
}

Używam tego we wszystkich metodach wyszukiwania elementów według identyfikatora, tekstu, elementu nadrzędnego itp .:

static ViewInteraction findById(int itemId) {
    ViewInteraction element = onView(withId(itemId));
    waitForElementUntilDisplayed(element);
    return element;
}
anna3101
źródło
w twoim przykładzie findById(int itemId)metoda zwróci element (który może mieć wartość NULL) niezależnie od tego, czy waitForElementUntilDisplayed(element);zwróci prawdę, czy fałsz ... więc to nie jest w porządku
mbob
Chciałem tylko się wtrącić i powiedzieć, że moim zdaniem jest to najlepsze rozwiązanie. IdlingResources nie są dla mnie wystarczające ze względu na 5-sekundową szczegółowość szybkości odpytywania (o wiele za duża jak na mój przypadek użycia). Zaakceptowana odpowiedź również dla mnie nie działa (wyjaśnienie, dlaczego jest już zawarte w długim kanale komentarzy dla tej odpowiedzi). Dzięki za to! Wziąłem twój pomysł i stworzyłem własne rozwiązanie i działa jak urok.
oaskamay
Tak, to jedyne rozwiązanie, które też mi się sprawdziło, gdy chcę czekać na elementy, których nie ma w bieżącej aktywności.
guilhermekrz
3

Espresso zostało stworzone, aby uniknąć wywołań funkcji sleep () w testach. Twój test nie powinien otwierać okna dialogowego do wprowadzenia adresu IP, za który powinna odpowiadać testowana aktywność.

Z drugiej strony test interfejsu użytkownika powinien:

  • Poczekaj, aż pojawi się okno dialogowe adresu IP
  • Wpisz adres IP i kliknij Enter
  • Poczekaj, aż pojawi się twój przycisk i kliknij go

Test powinien wyglądać mniej więcej tak:

// 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 czeka, aż wszystko, co dzieje się zarówno w wątku interfejsu użytkownika, jak iw puli AsyncTask, zakończy się przed wykonaniem testów.

Pamiętaj, że testy nie powinny robić niczego, co wiąże się z odpowiedzialnością aplikacji. Powinien zachowywać się jak „dobrze poinformowany użytkownik”: użytkownik, który klika, sprawdza, czy coś jest pokazane na ekranie, ale tak naprawdę zna identyfikatory komponentów

Bolhoso
źródło
2
Twój przykładowy kod jest zasadniczo tym samym kodem, który napisałem w swoim pytaniu.
Chad Bingham
@Binghammer mam na myśli to, że test powinien zachowywać się tak, jak zachowuje się użytkownik. Może brakuje mi tego, co robi twoja metoda IP.enterIP (). Czy możesz edytować swoje pytanie i to wyjaśnić?
Bolhoso
Moje komentarze mówią, co to robi. Jest to po prostu metoda w espresso, która wypełnia okno dialogowe adresu IP. To wszystko jest interfejsem użytkownika.
Chad Bingham
mm ... ok, więc masz rację, mój test zasadniczo robi to samo. Czy robisz coś poza wątkiem interfejsu użytkownika lub AsyncTasks?
Bolhoso,
16
Espresso nie działa tak, jak sugeruje kod i treść tej odpowiedzi. Wywołanie check w ViewInteraction nie będzie czekać, aż dany element dopasowujący się powiedzie, ale raczej zawiedzie natychmiast, jeśli warunek nie zostanie spełniony. Właściwym sposobem na to jest użycie AsyncTasks, jak wspomniano w tej odpowiedzi, lub, jeśli w jakiś sposób nie jest to możliwe, zaimplementowanie IdlingResource, który powiadomi UiController Espresso, kiedy można kontynuować wykonanie testu.
haffax
2

Powinieneś użyć zasobu Espresso Idling Resource, jest to zalecane w tym CodeLab

Bezczynny zasób reprezentuje operację asynchroniczną, której wyniki wpływają na kolejne operacje w teście interfejsu użytkownika. Rejestrując zasoby bezczynne w Espresso, możesz bardziej niezawodnie sprawdzać poprawność tych operacji asynchronicznych podczas testowania aplikacji.

Przykład wywołania asynchronicznego od prezentera

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

Zależności

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

W przypadku systemu Androidx

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

Oficjalne repozytorium: https://github.com/googlecodelabs/android-testing

Przykład IdlingResource: https://github.com/googlesamples/android-testing/tree/master/ui/espresso/IdlingResourceSample

Gastón Saillén
źródło
0

Chociaż myślę, że najlepiej jest do tego użyć zasobów Idling ( https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/ ), prawdopodobnie możesz użyć tego jako rozwiązania zastępczego:

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

a następnie nazwij go w swoim kodzie np .:

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

zamiast

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

Pozwala to również na dodawanie limitów czasu dla działań wyświetlania i potwierdzeń.

Piotr Zawadzki
źródło
Użyj poniższego pojedynczego wiersza kodu, aby zadać dowolny przypadek testowy Test Espresso: SystemClock.sleep (1000); // 1 Second
Nikunjkumar Kapupara
dla mnie to działa tylko przez zmianę tej linii return new TimedViewInteraction(Espresso.onView(viewMatcher));zreturn new TimedViewInteraction(Espresso.onView(viewMatcher).check(matches(isDisplayed())));
Manuel Schmitzberger
0

Moje narzędzie powtarza wykonywanie, które można uruchomić lub wywołać, dopóki nie przejdzie bez błędów lub nie zostanie wyrzucone po przekroczeniu limitu czasu. Doskonale sprawdza się w testach Espresso!

Załóżmy, że ostatnia interakcja z widokiem (kliknięcie przycisku) aktywuje niektóre wątki w tle (sieć, baza danych itp.). W rezultacie powinien pojawić się nowy ekran i chcemy to sprawdzić w następnym kroku, ale nie wiemy, kiedy nowy ekran będzie gotowy do testów.

Zalecanym podejściem jest wymuszenie na aplikacji wysyłania komunikatów o stanach wątków do testu. Czasami możemy użyć wbudowanych mechanizmów, takich jak OkHttp3IdlingResource. W innych przypadkach należy wstawić fragmenty kodu w różnych miejscach źródeł aplikacji (powinieneś znać logikę aplikacji!) Tylko do testowania wsparcia. Ponadto powinniśmy wyłączyć wszystkie Twoje animacje (chociaż jest to część interfejsu użytkownika).

Inne podejście czeka, np. SystemClock.sleep (10000). Ale nie wiemy, jak długo czekać, a nawet duże opóźnienia nie gwarantują sukcesu. Z drugiej strony twój test będzie trwał długo.

Moje podejście polega na dodaniu warunku czasu, aby wyświetlić interakcję. Np. Testujemy, że nowy ekran powinien pojawić się podczas 10000 mc (przekroczenie limitu czasu). Ale nie czekamy i sprawdzamy to tak szybko, jak chcemy (np. Co 100 ms) Oczywiście w ten sposób blokujemy wątek testowy, ale zwykle jest to właśnie to, czego potrzebujemy w takich przypadkach.

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

To jest źródło mojej klasy:

/**
 * 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
źródło
0

To jest pomocnik, którego używam w Kotlin do testów na Androida. W moim przypadku używam longOperation do naśladowania odpowiedzi serwera, ale możesz dostosować to do swojego celu.

@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
źródło
0

Dodam do tego mój sposób na zrobienie tego:

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

Nazywa się tak:

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

Do funkcji suspendUntilSuccess można dodać parametry, takie jak maksymalna liczba iteracji, długość iteracji itp.

Nadal wolę używać nieaktywnych zasobów, ale gdy testy działają z powodu np. Powolnych animacji na urządzeniu, używam tej funkcji i działa dobrze. Może oczywiście zawiesić się do 5 sekund, tak jak przed niepowodzeniem, więc może wydłużyć czas wykonywania testów, jeśli akcja, która zakończy się sukcesem, nigdy się nie powiedzie.

Sean Blahovici
źródło