Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testy UI w Espresso z farmą w tle

Avatar for MG MG
February 01, 2018

Testy UI w Espresso z farmą w tle

Razem z Mateuszem pokazujemy jak pisać testy UI na plaftormie Android oraz jak wykorzystać chmurę, by uruchamiać testy na różnych urządzeniach.

Avatar for MG

MG

February 01, 2018
Tweet

More Decks by MG

Other Decks in Programming

Transcript

  1. O czym będziemy mówili? • Espresso ◦ Custom View Action

    ◦ Custom View Matcher • PageObject
  2. O czym będziemy mówili? • Espresso ◦ Custom View Action

    ◦ Custom View Matcher • PageObject • Intent Matching/Stubing
  3. O czym będziemy mówili? • Espresso ◦ Custom View Action

    ◦ Custom View Matcher • PageObject • Intent Matching/Stubing • Idling Resource
  4. O czym będziemy mówili? • Espresso ◦ Custom View Action

    ◦ Custom View Matcher • PageObject • Intent Matching/Stubing • Idling Resource • Testy
  5. O czym będziemy mówili? • Espresso ◦ Custom View Action

    ◦ Custom View Matcher • PageObject • Intent Matching/Stubing • Idling Resource • Testy • Uruchamianie testów w chmurze - AWS Device Farm i Firebase Test Lab
  6. Espresso - Core API onView(withId(R.id.button)) ViewInteraction ViewMatcher NoMatchingViewException - gdy

    nie ma widoku, który opisaliśmy AmbiguousViewMatcherException - gdy nasz opis pasuje do dwóch i więcej widoków
  7. ViewMatchers - złożone allOf(Matcher<? super T>... matchers) - każdy z

    podanych allOf( withText("Help me!"), withHint("Attention"), hasSibling( withId(R.id.sos_button) ) )
  8. ViewMatchers - złożone anyOf(Matcher<? super T>... matchers) - chociaż jeden

    z podanych anyOf( withText("Help me!"), withHint("Attention"), hasSibling( withId(R.id.sos_button) ) )
  9. ViewMatchers • isDisplayed • isEnabled • hasFocus • isChecked •

    withParent • withChild • hasIMEAction • supportsInputMethod • hasErrorText • hasLinks • isAssignableFrom • isChecked • isClickable • ...
  10. ViewActions • doubleClick • longClick • swipeLeft • pressBack •

    pressBackUnconditionally • openLink • pressKey • pressMenuKey • scrollTo • swipeUp • typeText • ...
  11. Espresso - Core API ViewInteraction - Obiekt pośredni interakcji ViewMatcher

    - pozwala znajdować widoki ViewAction - pozwala wykonywać akcje na widokach ViewAssertion - pozwala opisywać wymagania dla widoku Cheat sheet - https://developer.android.com/training/testing/espresso/cheat-sheet.html
  12. Custom ViewActions - scroll and click public static ViewAction scrollAndClick()

    { return new ViewAction() { @Override public String getDescription() { return "Scroll and click"; } @Override public Matcher<View> getConstraints() { return ViewMatchers.isAssignableFrom(View.class); } @Override public void perform(UiController uiController, View view) { scrollTo().perform(uiController, view); click().perform(uiController, view); } }; }
  13. Custom ViewActions - type text and close keyboard public static

    ViewAction typeTextAndCloseKeyboard(String stringToBeTyped) { return new ViewAction() { @Override public void perform(UiController uiController, View view) { scrollTo().perform(uiController, view); clearText().perform(uiController, view); if (stringToBeTyped.length() > 0) { typeText(stringToBeTyped).perform(uiController, view); } closeSoftKeyboard().perform(uiController, view); } … }; }
  14. Custom ViewActions - type text and close keyboard ViewInteraction passwordInput

    = onView(withId(R.id.passwordTI)) passwordInput.perform( typeTextAndCloseKeyboard())
  15. Custom ViewActions - spinner void selectSpinnerItem(String text, Matcher<View> spinnerItemMatcher, ViewInteraction

    spinnerInteraction) { spinnerInteraction.perform(scrollAndClick()); try { onData(is(text)).perform(click()); } catch(Exception e) { // if spinner was not opened properly we must retry selectSpinnerItem(text, spinnerItemMatcher, spinnerInteraction); } onView(spinnerItemMatcher) // if spinner was not checked properly we must retry .withFailureHandler((error, viewMatcher) -> selectSpinnerItem(...)) .check(matches(withText(text))); }
  16. Custom ViewMatchers - podstawy BaseMatcher<T> - Klasa bazowa dla wszystkich

    matcherów TypeSafeMatcher<T> - null safe BoundedMatcher<T,S extends T> - Matcher(z Espresso) dla typu T, który pozwala nam zawęzić obiekt do pewnego podtypu T np. BoundedMatcher<View, TextView>
  17. Custom ViewMatchers - visibility public static Matcher<View> isVisible() { return

    new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(final View view){ return withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) .matches(view); } … }; }
  18. Custom ViewMatchers - has background color Matcher<View> hasBackgroundColor(@ColorRes final int

    colorId) { return new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(final View view) { if(view instanceof CardView) { return ((CardView)view).getCardBackgroundColor().getDefaultColor() == view.getContext().getResources().getColor(colorId); } return ((ColorDrawable) view.getBackground()).getColor() == view.getContext().getResources().getColor(colorId); } … }; }
  19. Custom ViewMatchers - ProgressBar ;) public static Matcher<View> hasProgress(final int

    progress) { return new BoundedMatcher<View, ProgressBar>(ProgressBar.class) { @Override public boolean matchesSafely(ProgressBar view) { return view.getProgress() == progress; } … }; }
  20. Page Object Jego zadaniem jest ukrycie niepotrzebnego skomplikowania dostępu do

    kontrolek, a uwypuklenie logiki biznesowej Najczęściej reprezentuje tylko elementy ważne z punktu widzenia użytkownika(Nie musi wiernie odwzorowywać całego ekranu)
  21. Page Object Jego zadaniem jest ukrycie niepotrzebnego skomplikowania dostępu do

    kontrolek, a uwypuklenie logiki biznesowej Najczęściej reprezentuje tylko elementy ważne z punktu widzenia użytkownika(Nie musi wiernie odwzorowywać całego ekranu) Powinien w odpowiedzi na metody zwracać albo typy proste albo inne Page Objecty
  22. Page Object Jego zadaniem jest ukrycie niepotrzebnego skomplikowania dostępu do

    kontrolek, a uwypuklenie logiki biznesowej Najczęściej reprezentuje tylko elementy ważne z punktu widzenia użytkownika(Nie musi wiernie odwzorowywać całego ekranu) Powinien w odpowiedzi na metody zwracać albo typy proste albo inne Page Objecty Może ale nie musi zawierać asercje dot. danego ekranu (zdania są podzielone)
  23. Page Object @Test public void openRegisterScreen() { onView(ViewMatchers.withId(R.id.registerButton)) .perform(click()); onView(ViewMatchers.withId(R.id.

    newUsernameEditText)) .check(matches(isDisplayed())); Espresso.closeSoftKeyboard(); Espresso.pressBack(); }
  24. Page Object @Test public void openRegisterScreen() { onView(ViewMatchers.withId(R.id.registerButton)) .perform(click()); onView(ViewMatchers.withId(R.id.

    newUsernameEditText)) .check(matches(isDisplayed())); Espresso.closeSoftKeyboard(); Espresso.pressBack(); onView(ViewMatchers.withId(R.id.usernameEditText)) .check(matches(isDisplayed())); }
  25. Page Object @Test public void openRegisterScreen() { onView(ViewMatchers.withId(R.id.registerButton)) .perform(click()); onView(ViewMatchers.withId(R.id.

    newUsernameEditText)) .check(matches(isDisplayed())); Espresso.closeSoftKeyboard(); Espresso.pressBack(); onView(ViewMatchers.withId(R.id.usernameEditText)) .check(matches(isDisplayed())); } @Test public void openRegisterScreen() { new LoginPageObject() .openRegisterScreen() .validate() .goBack() .validate(); }
  26. Page Object public class LoginPageObject { private final ViewInteraction usernameEditText;

    private final ViewInteraction passwordEditText; private final ViewInteraction loginButton; private final ViewInteraction registerButton; public LoginPageObject() { usernameEditText = onView(withId(R.id.usernameEditText)); passwordEditText = onView(withId(R.id.passwordEditText)); loginButton = onView(withId(R.id.loginButton)); registerButton = onView(withId(R.id.registerButton)); }
  27. Page Object public class LoginPageObject { private final ViewInteraction usernameEditText;

    private final ViewInteraction passwordEditText; private final ViewInteraction loginButton; private final ViewInteraction registerButton; public LoginPageObject() { usernameEditText = onView(withId(R.id.usernameEditText)); passwordEditText = onView(withId(R.id.passwordEditText)); loginButton = onView(withId(R.id.loginButton)); registerButton = onView(withId(R.id.registerButton)); } public ItemListPageObject login(String username, String password){ usernameEditText.perform(typeText(username)); passwordEditText.perform(typeText(password)); loginButton.perform(click()); return new ItemListPageObject(); }
  28. Page Object public class LoginPageObject { private final ViewInteraction usernameEditText;

    private final ViewInteraction passwordEditText; private final ViewInteraction loginButton; private final ViewInteraction registerButton; public LoginPageObject() { usernameEditText = onView(withId(R.id.usernameEditText)); passwordEditText = onView(withId(R.id.passwordEditText)); loginButton = onView(withId(R.id.loginButton)); registerButton = onView(withId(R.id.registerButton)); } public ItemListPageObject login(String username, String password) { usernameEditText.perform(typeText(username)); passwordEditText.perform(typeText(password)); loginButton.perform(click()); return new ItemListPageObject(); } public RegisterPageObject openRegisterScreen() { registerButton.perform(click()); return new RegisterPageObject(); }
  29. Page Object public class LoginPageObject { private final ViewInteraction usernameEditText;

    private final ViewInteraction passwordEditText; private final ViewInteraction loginButton; private final ViewInteraction registerButton; public LoginPageObject() { usernameEditText = onView(withId(R.id.usernameEditText)); passwordEditText = onView(withId(R.id.passwordEditText)); loginButton = onView(withId(R.id.loginButton)); registerButton = onView(withId(R.id.registerButton)); } public ItemListPageObject login(String username, String password) { usernameEditText.perform(typeText(username)); passwordEditText.perform(typeText(password)); loginButton.perform(click()); return new ItemListPageObject(); } public RegisterPageObject openRegisterScreen() { registerButton.perform(click()); return new RegisterPageObject(); } public LoginPageObject validate() { usernameEditText.check(matches(isDisplayed())); passwordEditText.check(matches(isDisplayed())); passwordEditText.check(matches(withInputType(INPUT_TYPE))); loginButton.check(matches(isDisplayed())); registerButton.check(matches(isDisplayed())); return this; }
  30. Page Object public class LoginPageObject { private final ViewInteraction usernameEditText;

    private final ViewInteraction passwordEditText; private final ViewInteraction loginButton; private final ViewInteraction registerButton; public LoginPageObject() { usernameEditText = onView(withId(R.id.usernameEditText)); passwordEditText = onView(withId(R.id.passwordEditText)); loginButton = onView(withId(R.id.loginButton)); registerButton = onView(withId(R.id.registerButton)); } public ItemListPageObject login(String username, String password) { usernameEditText.perform(typeText(username)); passwordEditText.perform(typeText(password)); loginButton.perform(click()); return new ItemListPageObject(); } public RegisterPageObject openRegisterScreen() { registerButton.perform(click()); return new RegisterPageObject(); } public LoginPageObject validate() { usernameEditText.check(matches(isDisplayed())); passwordEditText.check(matches(isDisplayed())); passwordEditText.check(matches(withInputType(INPUT_TYPE))); loginButton.check(matches(isDisplayed())); registerButton.check(matches(isDisplayed())); return this; } public LoginPageObject loginWithError(String username, String password) { login(username, password); return this; }
  31. Page Object public class LoginPageObject { private final ViewInteraction usernameEditText;

    private final ViewInteraction passwordEditText; private final ViewInteraction loginButton; private final ViewInteraction registerButton; public LoginPageObject() { usernameEditText = onView(withId(R.id.usernameEditText)); passwordEditText = onView(withId(R.id.passwordEditText)); loginButton = onView(withId(R.id.loginButton)); registerButton = onView(withId(R.id.registerButton)); } public ItemListPageObject login(String username, String password) { usernameEditText.perform(typeText(username)); passwordEditText.perform(typeText(password)); loginButton.perform(click()); return new ItemListPageObject(); } public RegisterPageObject openRegisterScreen() { registerButton.perform(click()); return new RegisterPageObject(); } public LoginPageObject validate() { usernameEditText.check(matches(isDisplayed())); passwordEditText.check(matches(isDisplayed())); passwordEditText.check(matches(withInputType(INPUT_TYPE))); loginButton.check(matches(isDisplayed())); registerButton.check(matches(isDisplayed())); return this; } public LoginPageObject loginWithError(String username, String password) { login(username, password); return this; } public LoginPageObject validateError() { onView(withText(R.string.loginError)).check(matches(isDisplayed())); return this; } }
  32. Klasa testowa - JUnit 4 @Rule - dostarcza rozszerzalny mechanizm

    zmiany zachowania testów ActivityTestRule - zapewnia “świeże” activity przed każdym testem ServiceTestRule - zapewnia “świeży” serwis przed każdym testem
  33. Klasa testowa - JUnit 4 @Rule - dostarcza rozszerzalny mechanizm

    zmiany zachowania testów ActivityTestRule - zapewnia “świeże” activity przed każdym testem ServiceTestRule - zapewnia “świeży” serwis przed każdym testem @RunWith - pozwala ustawić runner dla klasy testowej
  34. Klasa testowa - JUnit 4 @Rule - dostarcza rozszerzalny mechanizm

    zmiany zachowania testów ActivityTestRule - zapewnia “świeże” activity przed każdym testem ServiceTestRule - zapewnia “świeży” serwis przed każdym testem @RunWith - pozwala ustawić runner dla klasy testowej @Before - oznaczenie metody wywoływanej przed każdym testem
  35. Klasa testowa - JUnit 4 @Rule - dostarcza rozszerzalny mechanizm

    zmiany zachowania testów ActivityTestRule - zapewnia “świeże” activity przed każdym testem ServiceTestRule - zapewnia “świeży” serwis przed każdym testem @RunWith - pozwala ustawić runner dla klasy testowej @Before - oznaczenie metody wywoływanej przed każdym testem @After - oznaczenie metody wywoływanej po każdym teście
  36. Klasa testowa - JUnit 4 @Rule - dostarcza rozszerzalny mechanizm

    zmiany zachowania testów ActivityTestRule - zapewnia “świeże” activity przed każdym testem ServiceTestRule - zapewnia “świeży” serwis przed każdym testem @RunWith - pozwala ustawić runner dla klasy testowej @Before - oznaczenie metody wywoływanej przed każdym testem @After - oznaczenie metody wywoływanej po każdym teście @Test - oznaczenie metody testowej
  37. @RunWith(AndroidJUnit4.class) public class IncreaseCreditLineLimitTests { @Rule public ActivityTestRule<LauncherActivity> activityRule =

    new ActivityTestRule<>(LauncherActivity.class, false, false); … @Before public void setUp() { TestCredwayApp.reloadComponent(); TestCredwayApp.getComponent().inject(this); Espresso.registerIdlingResources(idlingResource); userSessionSimulator.cleanSession(); } @After public void tearDown() { Espresso.unregisterIdlingResources(idlingResource); } @Test public void increaseLimitSuccessWithRatingDialog() { String currentCreditLineLimitText = "15 000 kr"; String chosenCreditLineLimitText = "16 000 kr"; … } }
  38. Intent Weryfikacja intentów(matching) - sprawdzenie czy dany Intent został wysłany

    intended(allOf( hasAction(equalTo(Intent.ACTION_VIEW)), hasCategories(hasItem(equalTo(Intent.CATEGORY_BROWSABLE))), hasData(hasHost(equalTo("www.google.com"))), hasExtras(allOf( hasEntry(equalTo("key1"), equalTo("value1")), hasEntry(equalTo("key2"), equalTo("value2")))), toPackage("com.android.browser")));
  39. Intent Stubowanie intentów, pozwala zwrócić odpowiednio spreparowaną odpowiedź na dopasowany

    intent. Intent resultData = new Intent(); String phoneNumber = "123-345-6789"; resultData.putExtra("phone", phoneNumber); ActivityResult result = new ActivityResult(Activity.RESULT_OK, resultData); intending(toPackage("com.android.contacts")).respondWith(result);
  40. Intent @Rule public IntentsTestRule<MyPagesActivity> testRule = new IntentsTestRule<>(MyPagesActivity.class, true, false);

    @Inject UserSessionSimulator userSessionSimulator; @Before public void setUp() { TestCredwayApp.reloadComponent(); TestCredwayApp.getComponent().inject(this); userSessionSimulator.simulateUserSession(); } @Test public void shouldStartDialerIntent() { testRule.launchActivity(null); mockDialerIntent(); new MyPagesPageObject() .openDrawer() .openPhone(); verifyCalledDialerIntentWithPhoneUri("tel:08363773"); }
  41. Intent @Rule public IntentsTestRule<MyPagesActivity> testRule = new IntentsTestRule<>(MyPagesActivity.class, true, false);

    @Inject UserSessionSimulator userSessionSimulator; @Before public void setUp() { TestCredwayApp.reloadComponent(); TestCredwayApp.getComponent().inject(this); userSessionSimulator.simulateUserSession(); } @Test public void shouldStartDialerIntent() { testRule.launchActivity(null); mockDialerIntent(); new MyPagesPageObject() .openDrawer() .openPhone(); verifyCalledDialerIntentWithPhoneUri("tel:08363773"); }
  42. Intent @Rule public IntentsTestRule<MyPagesActivity> testRule = new IntentsTestRule<>(MyPagesActivity.class, true, false);

    @Test public void shouldStartDialerIntent() { testRule.launchActivity(null); mockDialerIntent(); new MyPagesPageObject() .openDrawer() .openPhone(); verifyCalledDialerIntentWithPhoneUri("tel:08363773"); }
  43. Intent @Rule public IntentsTestRule<MyPagesActivity> testRule = new IntentsTestRule<>(MyPagesActivity.class, true, false);

    @Test public void shouldStartDialerIntent() { testRule.launchActivity(null); intending(hasAction(Intent.ACTION_DIAL)) .respondWith(new ActivityResult(Activity.RESULT_OK, null)); new MyPagesPageObject() .openDrawer() .openPhone(); intended( allOf( hasAction(Intent.ACTION_DIAL), hasData("tel:08363773") ) ); }
  44. Idling Resource Dostępne są cztery podstawowe klasy pozwalające na synchronizację

    Espresso z naszym kodem: • CountingIdlingResource • UriIdlingResource
  45. Idling Resource Dostępne są cztery podstawowe klasy pozwalające na synchronizację

    Espresso z naszym kodem: • CountingIdlingResource • UriIdlingResource • IdlingThreadPoolExecutor
  46. Idling Resource Dostępne są cztery podstawowe klasy pozwalające na synchronizację

    Espresso z naszym kodem: • CountingIdlingResource • UriIdlingResource • IdlingThreadPoolExecutor • IdlingScheduledThreadPoolExecutor
  47. Idling Resources public final class RxEspressoTransformer { private final Observable.Transformer

    transformer; public RxEspressoTransformer(CountingIdlingResource idlingResource) { transformer = new Observable.Transformer<Observable, Observable>() { @Override public Observable<Observable> call(Observable<Observable> observableObservable) { return observableObservable .doOnSubscribe(idlingResource::increment) .doAfterTerminate(idlingResource::decrement); } }; } public <T> Observable.Transformer<T, T> apply() { return (Observable.Transformer<T, T>) transformer; } }
  48. Idling Resources public final class RxEspressoTransformer { private final Observable.Transformer

    transformer; public RxEspressoTransformer(CountingIdlingResource idlingResource) { transformer = new Observable.Transformer<Observable, Observable>() { @Override public Observable<Observable> call(Observable<Observable> observableObservable) { return observableObservable .doOnSubscribe(idlingResource::increment) .doAfterTerminate(idlingResource::decrement); } }; } public <T> Observable.Transformer<T, T> apply() { return (Observable.Transformer<T, T>) transformer; } }
  49. Idling Resources observable .compose(espressoTransformer.apply()) … public final class RxEspressoTransformer {

    private final Observable.Transformer transformer; public RxEspressoTransformer(CountingIdlingResource idlingResource) { transformer = new Observable.Transformer<Observable, Observable>() { @Override public Observable<Observable> call(Observable<Observable> observableObservable) { return observableObservable .doOnSubscribe(idlingResource::increment) .doAfterTerminate(idlingResource::decrement); } }; } public <T> Observable.Transformer<T, T> apply() { return (Observable.Transformer<T, T>) transformer; } }
  50. Idling Resources observable .compose(espressoTransformer.apply()) … public final class RxEspressoTransformer {

    private final Observable.Transformer transformer; public RxEspressoTransformer(CountingIdlingResource idlingResource) { transformer = new Observable.Transformer<Observable, Observable>() { @Override public Observable<Observable> call(Observable<Observable> observableObservable) { return observableObservable .doOnSubscribe(idlingResource::increment) .doAfterTerminate(idlingResource::decrement); } }; } public <T> Observable.Transformer<T, T> apply() { return (Observable.Transformer<T, T>) transformer; } } public class IncreaseCreditLineLimitTests { … @Inject CountingIdlingResource idlingResource; @Before public void setUp() { … Espresso.registerIdlingResources(idlingResource); … } … }
  51. Idling Resources observable .compose(espressoTransformer.apply()) … public final class RxEspressoTransformer {

    private final Observable.Transformer transformer; public RxEspressoTransformer(CountingIdlingResource idlingResource) { transformer = new Observable.Transformer<Observable, Observable>() { @Override public Observable<Observable> call(Observable<Observable> observableObservable) { return observableObservable .doOnSubscribe(idlingResource::increment) .doAfterTerminate(idlingResource::decrement); } }; } public <T> Observable.Transformer<T, T> apply() { return (Observable.Transformer<T, T>) transformer; } } public class IncreaseCreditLineLimitTests { … @Inject CountingIdlingResource idlingResource; @Before public void setUp() { … Espresso.registerIdlingResources(idlingResource); … } … }
  52. Test ekranu formularza @Rule public ActivityTestRule<ApplyFormActivity> activityRule = new ActivityTestRule<>(ApplyFormActivity.class);

    … @Test public void validateShowErrorOnWrongEmail() { new ApplyFormPageObject() .typePersonalNumber("198911069998") .typeMobileNumber("0786545678") .typeEmail("test@email") .typeClearingNumber("5555") .typeAccountNumber("8911069998") .selectIncome("1 000 kr+") .selectHousing("Home") .selectHousingCost("1 000 kr+") .selectOtherLoansCost("1 000 kr+") .selectEmploymentForm("Freelancer") .selectChildrenCount("0+") .clickOnApplyButtonWrongInput() .validateWrongEmailError(); }
  53. Test procesu rejestracji @Rule public ActivityTestRule<LauncherActivity> activityRule = new ActivityTestRule<>(LauncherActivity.class);

    @Test public void applyForCreditLineConfirmationStep() { new WelcomePageObject() .validate() .clickOnRegistrationButton() .validate("30 000 kr", 21, 21) .clickOnApplyButton() .validate("30 000 kr", "10 kr") … (wypełnianie formularza tak samo jak w poprzednim teście) … .clickOnPositiveButtonConfirmation() .validate() .onBackClick() .validate(); }