Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Solution ü Architectural patterns (MVP, MVVM, etc) ü Automated testing ü Test coverage ü Continuous Integration

Slide 4

Slide 4 text

App

Slide 5

Slide 5 text

Types of testing Unit testing –JUnit –Mockito –PowerMockito –etc UI testing –Espresso –Robotium –Robolectric –UI Automator –etc

Slide 6

Slide 6 text

Android project structure Source code module/src/main/java Unit tests module/src/test/java Instrumentation tests module/src/androidTest/java

Slide 7

Slide 7 text

Category of Android tests Run on the JVM (local tests) Require Android device (instrumentation tests) Tests

Slide 8

Slide 8 text

Running local tests in Android Studio Gradle ./gradlew test Result of tests: {MODULE}/build/reports/tests/ Run menu Run / Edit Configurations… Project tree

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

UI Automator Viewer

Slide 11

Slide 11 text

UI Automator Viewer Ø Android Studio Tools / Android / Android Device Monitor Ø Application {ANDROID_SDK}/ tools / uiautomatorviewer

Slide 12

Slide 12 text

Espresso

Slide 13

Slide 13 text

What is Espresso? Ø Android UI test framework Ø Compatible with Android 2.2 (API level 8) or higher Ø Part of Android Testing Support Library

Slide 14

Slide 14 text

Why is Espresso? Ø Easy to setup Ø Readable tests Ø Extendable Ø Smart waiting

Slide 15

Slide 15 text

Install / Update Android Support Libraries Ø Android SDK Manager Ø Terminal

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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/

Slide 18

Slide 18 text

Espresso UI tests onView(ViewMatcher) .perform(ViewAction) .check(ViewAssertion); onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion);

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

First test case @RunWith(AndroidJUnit4.class) public class MainActivityEspressoTest { @Rule public ActivityTestRule 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"))); ... } }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

isRoot matcher public static Matcher isRoot() { return new TypeSafeMatcher() { @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; } }; }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

AdapterView java.lang.Object ↳ android.view.View ↳ android.view.ViewGroup ↳ android.widget.AdapterView https://developer.android.com/reference/android/widget/AdapterView.html

Slide 30

Slide 30 text

onData onData(ObjectMatcher) .DataOptions .perform(ViewAction) .check(ViewAssertion)

Slide 31

Slide 31 text

onData – ArrayAdapter onData(allOf(is(instanceOf(String.class)), is("Americano"))) .check(matches(isDisplayed())); ArrayAdapter adapter = new ArrayAdapter<>( this, android.R.layout.simple_list_item_1, android.R.id.text1, coffeeList);

Slide 32

Slide 32 text

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]; } }

Slide 33

Slide 33 text

withCoffee matcher public static Matcher withCoffee(final Coffee coffee) { return new BoundedMatcher(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); } }; }

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

RecyclerViewActions Ø actionOnHolderItem Ø actionOnItem Ø actionOnItemAtPosition Ø scrollTo Ø scrollToHolder Ø scrollToPosition https://developer.android.com/reference/android/support/test/espresso/contrib/RecyclerViewActions.html

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Configure custom intents @Rule public ActivityTestRule 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); ... }

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Using IdlingResource @Before public void setup() { mServiceIdlingResource = new ServiceIdlingResource(InstrumentationRegistry.getTargetContext()); registerIdlingResources(mServiceIdlingResource); } @Test public void shouldOrderCoffee() { ... } @After public void tearDown() { unregisterIdlingResources(mServiceIdlingResource); }

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

withToolbarTitle matcher public static Matcher withToolbarTitle(final Matcher textMatcher) { return new BoundedMatcher(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); } }; }

Slide 43

Slide 43 text

UI Automator

Slide 44

Slide 44 text

What is UI Automator? Ø Android UI test framework Ø Part of Android Testing Support Library Ø Interacting with any applications Ø Interacting with a system

Slide 45

Slide 45 text

Why is UI Automator? Ø Easy to setup Ø Readable tests Ø Access to device state Ø UI Automator requires Android 4.3 (API 18) or higher

Slide 46

Slide 46 text

Install / Update Android Support Libraries Ø Android SDK Manager Ø Terminal

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Integrate UI Automator to the project (API < 18) Manifest Test annotation @SdkSuppress(minSdkVersion = 18) public class OrderCoffeeActivity { @Test public void shouldOrderThreeEspresso() { ... } }

Slide 49

Slide 49 text

Basic UI Automator components Ø UiDevice Ø UiCollection Ø UiObject / UiObject2 Ø UiScrollable Ø UiSelector Ø Configurator

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Espresso + UI Automator

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Tips

Slide 55

Slide 55 text

Do not copy your code between tests

Slide 56

Slide 56 text

Use available stuff

Slide 57

Slide 57 text

Use Hamcrest library

Slide 58

Slide 58 text

Focus on app behavior

Slide 59

Slide 59 text

Many small tests better than big one

Slide 60

Slide 60 text

Launch directly desired screen

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

Any Questions? @Alex_Zhukovich