Android UI testing for beginners

Android UI testing for beginners

Introduction to Android UI testing.

Presentation Details:
------------------------------------------------------
Mobile apps are growing. Apps become more complex and require more testing. It means that it’s time to integrate automating tests to your project. What do you have for mobile applications?

This talk would be an intro to some of the feature of Espresso and UI Automator frameworks.
- General Android project problems
- General Android testing problem
- Overview of UI Automator Viewer
- Espresso and UI Automator allow you test UI part of your application and how your app interacts with Android OS
- Custom Espresso matchers

Source code:
https://github.com/AlexZhukovich/SimpleCoffeeOrderTestProject

Blog:
http://alexzh.com

2b0404a5db1a74f01bf3bf94d142e28c?s=128

Alex Zhukovich

June 30, 2016
Tweet

Transcript

  1. Android UI testing for beginners @Alex_Zhukovich http://alexzh.com

  2. Observations Ø Android has no clear way to structure of

    an application Ø Huge activities / fragments Ø Hard to add features Ø Hard to test Ø Many apps tested in manual way
  3. Solution ü Architectural patterns (MVP, MVVM, etc) ü Automated testing

    ü Test coverage ü Continuous Integration
  4. App

  5. Types of testing Unit testing –JUnit –Mockito –PowerMockito –etc UI

    testing –Espresso –Robotium –Robolectric –UI Automator –etc
  6. Android project structure Source code module/src/main/java Unit tests module/src/test/java Instrumentation

    tests module/src/androidTest/java
  7. Category of Android tests Run on the JVM (local tests)

    Require Android device (instrumentation tests) Tests
  8. Running local tests in Android Studio Gradle ./gradlew test Result

    of tests: {MODULE}/build/reports/tests/ Run menu Run / Edit Configurations… Project tree
  9. Running instrumentation tests in Android Studio Gradle ./gradlew connectedAndroidTest ./gradlew

    cAT Result of tests: {MODULE}/build/reports/ androidTests/connected/ Run menu Run / Edit Configurations… Project tree
  10. UI Automator Viewer

  11. UI Automator Viewer Ø Android Studio Tools / Android /

    Android Device Monitor Ø Application {ANDROID_SDK}/ tools / uiautomatorviewer
  12. Espresso

  13. What is Espresso? Ø Android UI test framework Ø Compatible

    with Android 2.2 (API level 8) or higher Ø Part of Android Testing Support Library
  14. Why is Espresso? Ø Easy to setup Ø Readable tests

    Ø Extendable Ø Smart waiting
  15. Install / Update Android Support Libraries Ø Android SDK Manager

    Ø Terminal
  16. Integrate Espresso to the project android { defaultConfig { testInstrumentationRunner

    "android.support.test.runner.AndroidJUnitRunner" } } ... dependencies { ... androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2' androidTestCompile 'com.android.support.test.runner:0.5' }
  17. Recommendation for setting up the device / emulator One small

    recommendation for avoiding flakiness is turning off animation on real or virtual devices. • Window animation scale • Transition animation scale • Animator duration scale • Settings / Developer Options/
  18. Espresso UI tests onView(ViewMatcher) .perform(ViewAction) .check(ViewAssertion); onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion);

  19. ViewMatcher onView(withId(R.id.pay_button)) ← ViewMatcher .perform(ViewAction) .check(ViewAssertion);

  20. ViewAction onView(withId(R.id.pay_button)) .perform(click()) ← ViewAction .check(ViewAssertion);

  21. ViewAssertion onView(withId(R.id.pay_button)) .perform(click()) .check(matches(isDisplayed())); ← ViewAssertion

  22. Is it pass? @Test public void testSomeView() { onView(withId(R.id.test)); }

  23. Hamcrest http://www.marcphilipp.de/downloads/posts/2013-01-02-hamcrest-quick-reference/Hamcrest-1.3.pdf

  24. First test case @RunWith(AndroidJUnit4.class) public class MainActivityEspressoTest { @Rule public

    ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class); @Test public void validateNumberPicker() { ... onView(withId(R.id.coffee_increment)) .perform(click()); onView(withId(R.id.coffee_count)) .check(matches(withText("1"))); ... } }
  25. Snackbar @Test public void shouldDisplayOrderMoreCoffeeSnackBar() { onView(withId(R.id.pay)) .perform(click()); onView(withId(android.support.design.R.id.snackbar_text)) .check(matches(withText(R.string.message_more_coffee)));

    }
  26. Toast @Test public void shouldDisplayOrderMoreCoffeeToast() { onView(withId(R.id.pay)) .perform(click()); onView(withText(R.string.message_more_coffee)) .inRoot(isRoot())

    .check(matches(isDisplayed())); }
  27. isRoot matcher public static Matcher<Root> isRoot() { return new TypeSafeMatcher<Root>()

    { @Override public void describeTo(Description description) { description.appendText("is toast"); } @Override protected boolean matchesSafely(Root root) { if ((root.getWindowLayoutParams().get().type == WindowManager.LayoutParams.TYPE_TOAST)) { IBinder windowToken = root.getDecorView().getWindowToken(); IBinder appToken = root.getDecorView().getApplicationWindowToken(); if (windowToken == appToken) { return true; } } return false; } }; }
  28. Enter text @Test public void shouldVerifyOrderAndEnterUsername() { ... onView(withId(R.id.comment_toolbar)) .perform(typeText("Alex"),

    closeSoftKeyboard()); onView(withId(R.id.comment_toolbar)) .check(matches(withText("Alex"))); }
  29. AdapterView java.lang.Object ↳ android.view.View ↳ android.view.ViewGroup ↳ android.widget.AdapterView <T extends

    android.widget.Adapter> https://developer.android.com/reference/android/widget/AdapterView.html
  30. onData onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion)

  31. onData – ArrayAdapter onData(allOf(is(instanceOf(String.class)), is("Americano"))) .check(matches(isDisplayed())); ArrayAdapter<String> adapter = new

    ArrayAdapter<>( this, android.R.layout.simple_list_item_1, android.R.id.text1, coffeeList);
  32. onData – custom adapter @Test public void clickCoffeeItem() { ...

    onData(withCoffee(espresso)) .inAdapterView(withId(R.id.detail_list)) .perform(click()); } public class CoffeeAdapter extends BaseAdapter { ... @Override public Object getItem(int position) { return mOrderedCoffee.keySet().toArray()[position]; } }
  33. withCoffee matcher public static Matcher<Object> withCoffee(final Coffee coffee) { return

    new BoundedMatcher<Object, Coffee>(Coffee.class) { @Override public void describeTo(Description description) { description.appendText("has value: "); description.appendText(coffee.toString()); } @Override protected boolean matchesSafely(Coffee item) { return item.equals(coffee); } }; }
  34. RecyclerView java.lang.Object ↳ android.view.View ↳ android.view.ViewGroup ↳ android.support.v7.widget.RecyclerView It is

    not an AdapterView https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
  35. RecyclerView dependencies { ... androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2') { // Necessary to avoid

    version conflicts exclude group: 'com.android.support', module: 'appcompat' exclude group: 'com.android.support', module: 'support-v4' exclude group: 'com.android.support', module: 'support-annotations' exclude module: 'recyclerview-v7' } } onView(withId(recyclerViewId)) .perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
  36. RecyclerViewActions Ø actionOnHolderItem Ø actionOnItem Ø actionOnItemAtPosition Ø scrollTo Ø

    scrollToHolder Ø scrollToPosition https://developer.android.com/reference/android/support/test/espresso/contrib/RecyclerViewActions.html
  37. Configure custom intents @Rule public ActivityTestRule<PaymentActivity> mActivityRule = new ActivityTestRule<PaymentActivity>(PaymentActivity.class)

    { @Override protected Intent getActivityIntent() { Intent result = new Intent(...); result.putExtra(...); return result; } };
  38. Configure custom intents @Rule public ActivityTestRule<PaymentActivity> mActivityRule = new ActivityTestRule<>(

    PaymentActivity.class, true, false); // is activity launch? @Test public void shouldTestSomething() { Context context = InstrumentationRegistry.getTargetContext(); Intent intent = new Intent(context, MainActivity.class); intent.putExtra(PaymentActivity.TOTAL_PRICE, 5.0f); mActivityRule.launchActivity(intent); ... }
  39. IdlingResource public class ServiceIdlingResource implements IdlingResource { public ServiceIdlingResource(Context context)

    { this.mContext = context; } @Override public String getName() { return ServiceIdlingResource.class.getSimpleName(); } @Override public boolean isIdleNow() { boolean idle = !isIntentServiceRunning(); if (idle && mCallback != null) { mCallback.onTransitionToIdle(); } return idle; } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { this.mCallback = callback; } private boolean isIntentServiceRunning() { ... } } http://blog.sqisland.com/2015/04/espresso-custom-idling-resource.html
  40. Using IdlingResource @Before public void setup() { mServiceIdlingResource = new

    ServiceIdlingResource(InstrumentationRegistry.getTargetContext()); registerIdlingResources(mServiceIdlingResource); } @Test public void shouldOrderCoffee() { ... } @After public void tearDown() { unregisterIdlingResources(mServiceIdlingResource); }
  41. Basic application test @Test public void shouldCheckIntentValues() { ... mActivityRule.launchActivity(intent);

    onView(withId(R.id.anim_toolbar)) .check(matches( withToolbarTitle(is(espresso.getName())))); onView(withId(R.id.recyclerView)) .check(matches(atPosition(0, context.getString(R.string.ingredients), ingredients.toString()))); ... }
  42. withToolbarTitle matcher public static Matcher<Object> withToolbarTitle(final Matcher<String> textMatcher) { return

    new BoundedMatcher<Object, Toolbar>(Toolbar.class) { @Override public boolean matchesSafely(Toolbar toolbar) { return textMatcher.matches(toolbar.getTitle()); } @Override public void describeTo(Description description) { description.appendText("with toolbar title: "); textMatcher.describeTo(description); } }; }
  43. UI Automator

  44. What is UI Automator? Ø Android UI test framework Ø

    Part of Android Testing Support Library Ø Interacting with any applications Ø Interacting with a system
  45. Why is UI Automator? Ø Easy to setup Ø Readable

    tests Ø Access to device state Ø UI Automator requires Android 4.3 (API 18) or higher
  46. Install / Update Android Support Libraries Ø Android SDK Manager

    Ø Terminal
  47. Integrate UI Automator to the project android { defaultConfig {

    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } } ... dependencies { ... androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1' }
  48. Integrate UI Automator to the project (API < 18) Manifest

    <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.alexzh.simplecoffeeorder"> <uses-sdk android:minSdkVersion="18" android:targetSdkVersion="23" tools:overrideLibrary="android.support.test.uiautomator.v18"/> </manifest> Test annotation @SdkSuppress(minSdkVersion = 18) public class OrderCoffeeActivity { @Test public void shouldOrderThreeEspresso() { ... } }
  49. Basic UI Automator components Ø UiDevice Ø UiCollection Ø UiObject

    / UiObject2 Ø UiScrollable Ø UiSelector Ø Configurator
  50. UiDevice Press "Home" button mUiDevice.pressHome(); Press "Back" button mUiDevice.pressBack(); Press

    at power button mUiDevice.wakeUp(); Click at arbitrary coordinates mUiDevice.click(300, 215); Open the notifications mUiDevice.openNotification();
  51. Test for calculator application @Test public void shouldSumTwoNumbers() throws UiObjectNotFoundException

    { mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); mDevice.pressHome(); UiObject2 allApps = mDevice.findObject(By.desc("Apps")); allApps.click(); UiObject2 calculatorApp = mDevice.findObject(By.desc("Calculator")); calculatorApp.click(); mDevice.wait(Until.hasObject(By.text("5")), 3000); UiObject2 two = mDevice.findObject(By.text("2")); UiObject2 three = mDevice.findObject(By.text("3")); ... two.click(); plus.click(); three.click(); equal.click(); UiObject2 result = mDevice.findObject(By.res("com.android.calculator2", "formula")); assertEquals("5", result.getText()); }
  52. Espresso + UI Automator

  53. Example @RunWith(AndroidJUnit4.class) public class FullOrderTest { ... @Test public void

    shouldOrderThreeEspressos() { ... onView(withId(R.id.pay)).perform(click()); mDevice.openNotification(); mDevice.wait(Until.hasObject(By.text(notificationTitle)), 3000); UiObject2 title = mDevice.findObject(By.text(notificationTitle)); assertEquals(notificationTitle, title.getText()); ... } }
  54. Tips

  55. Do not copy your code between tests

  56. Use available stuff

  57. Use Hamcrest library

  58. Focus on app behavior

  59. Many small tests better than big one

  60. Launch directly desired screen

  61. Links • Google samples – Android architecture https://github.com/googlesamples/android-architecture • Espresso

    https://google.github.io/android-testing-support-library/docs/espresso/index.html • Testing UI for Multiple Apps https://developer.android.com/training/testing/ui-testing/uiautomator-testing.html • SimpleCoffeeOrderTestProject https://github.com/AlexZhukovich/SimpleCoffeeOrderTestProject • Introduction to Android testing http://alexzh.com/tag/testing/ • Android Testing Codelab https://codelabs.developers.google.com/codelabs/android-testing/ @Alex_Zhukovich
  62. Any Questions? @Alex_Zhukovich