Slide 1

Slide 1 text

Efficient Android UI Testing @alex_zhukovich https://alexzh.com/

Slide 2

Slide 2 text

ANDROID TESTING UI & NON-UI TESTS DEVICES

Slide 3

Slide 3 text

Local and remote data sources. DATA LAYERS Vendors produce devices with different OS versions. FRAGMENTATION Phones, Tablets, Watches, TVs and custom devices. DIFFERENT DEVICES Notifications, Widgets, Runtime permissions, App Shortcuts, etc. OS FEATURES

Slide 4

Slide 4 text

APPLICATION IS GROWING Users Features

Slide 5

Slide 5 text

LOCAL UI* Non-UI INSTRUMENTATION UI Non-UI ANDROID TESTS

Slide 6

Slide 6 text

APPROACHES TOOLS LAYOUT INSPECTOR UI TEST RECORDERS FRAMEWORKS TYPES OF UI TESTS

Slide 7

Slide 7 text

VIEW INFORMATION

Slide 8

Slide 8 text

LAYOUT INSPECTOR DETAIL VIEW INFORMATION We can get information about text, color, alpha, margins, paddings, etc. PIXEL PERFECTNESS We can use built-in tools for comparing design and rendered layouts. 3D VISUALIZATION We can analyze layouts of the application in 3D to a better understanding of nesting layouts.

Slide 9

Slide 9 text

ANALYZE ANY APP We can analyze any application without root permissions. UI AUTOMATOR VIEWER

Slide 10

Slide 10 text

TEST RECORDER

Slide 11

Slide 11 text

APPIUM RECORDER CREATE APPIUM TESTS It allows us to create simple Appium tests in few clicks. GENERATE CODE Appium Recorder generates a code of test case that use an Appium framework. PROBLEM WITH WAITING Generated code has a problem with waiting data from web servers, etc.

Slide 12

Slide 12 text

GENERATED CODE BY APPIUM RECORDER public class SampleTest { private AndroidDriver driver; @Before public void setUp() throws MalformedURLException { DesiredCapabilities desiredCapabilities = new DesiredCapabilities(); desiredCapabilities.setCapability("platformName", "Android"); desiredCapabilities.setCapability("ensureWebviewsHavePages", true); URL remoteUrl = new URL("http://localhost:4723/wd/hub"); driver = new AndroidDriver(remoteUrl, desiredCapabilities); } @Test public void sampleTest() { MobileElement el7 = (MobileElement) driver.findElementById("com.alexzh.imbarista:id/emailEditText"); el7.sendKeys("[email protected]"); MobileElement el8 = (MobileElement) driver.findElementById("com.alexzh.imbarista:id/passwordEditText"); el8.sendKeys("test"); MobileElement el9 = (MobileElement) driver.findElementById("com.alexzh.imbarista:id/loginButton"); el9.click(); MobileElement el10 = (MobileElement) driver.findElementByAccessibilityId("Profile"); el10.click(); MobileElement el11 = (MobileElement) driver.findElementByAccessibilityId("More options"); el11.click(); MobileElement el12 = (MobileElement) driver.findElementByXPath("/hierarchy/android.widget.FrameLayout/android.widget.FrameLayout/ android.widget.ListView/android.widget.LinearLayout[2]/android.widget.LinearLayout/android.widget.RelativeLayout/android.widget.TextView"); el12.click(); } @After public void tearDown() { driver.quit(); } }

Slide 13

Slide 13 text

CREATE ESPRESSO TESTS It allows us to create simple Espresso tests in few clicks. GENERATE CODE Espresso Test Recorder generates a code of test case that use an Espresso framework. PROBLEM WITH WAITING Generated code has a problem with waiting data from web servers, etc ESPRESSO TEST RECORDER

Slide 14

Slide 14 text

GENERATED CODE BY ESPRESSO TEST RECORDER @RunWith(AndroidJUnit4.class) public class RecorderActivityTest { @Rule public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(SplashActivity.class); @Test public void recorderActivityTest() { ViewInteraction textInputEditText = onView( allOf(withId(R.id.emailEditText), childAtPosition(childAtPosition(withId(R.id.emailInputLayout),0),0))); textInputEditText.perform(scrollTo(), replaceText("[email protected]"), closeSoftKeyboard()); ViewInteraction textInputEditText2 = onView( allOf(withId(R.id.passwordEditText),childAtPosition(childAtPosition(withId(R.id.passwordInputLayout), 0), 0))); textInputEditText2.perform(scrollTo(), replaceText("test"), closeSoftKeyboard()); ViewInteraction materialButton = onView( allOf(withId(R.id.loginButton), withText("Login"), childAtPosition(childAtPosition(withId(R.id.scrollView2), 0), 4))); materialButton.perform(scrollTo(), click()); ViewInteraction bottomNavigationItemView = onView( allOf( withId(R.id.navigation_profile), withContentDescription("Profile"), childAtPosition(childAtPosition(withId(R.id.navigation), 0), 2), isDisplayed() )); bottomNavigationItemView.perform(click()); ViewInteraction overflowMenuButton = onView( allOf(withContentDescription("More options"), childAtPosition(childAtPosition(withId(R.id.toolbar), 1), 0), isDisplayed())); overflowMenuButton.perform(click()); ViewInteraction appCompatTextView = onView( allOf(withId(R.id.title), withText("Log Out"), childAtPosition(childAtPosition(withId(R.id.content), 0), 0), isDisplayed())); appCompatTextView.perform(click()); } ... }

Slide 15

Slide 15 text

UI TESTING FRAMEWORKS

Slide 16

Slide 16 text

ESPRESSO APPIUM UI AUTOMATOR

Slide 17

Slide 17 text

ESPRESSO START ANY ACTIVITY Espresso allows us to start a test from any Activity. We can also test any fragment in isolation with an additional library. SYNCHRONIZATION Espresso performs synchronization with an app after each onView call. IDLING RESOURCES The Idling Resources mechanism allows the Espresso to wait until the asynchronous operation is finished. onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed()))

Slide 18

Slide 18 text

INTERACTION WITH ANY APP Ui Automator framework can interact with any application installed on the device/emulator. INTERACTION WITH OS Ui Automator framework can interact with OS features, like notification, app shortcuts, permissions, etc. INTERACTION WITH DEVICE Ui Automator framework can interact with device buttons, emulate screen rotation, etc. UI AUTOMATOR val email = device.findObject(By.res(PACKAGE, "email")) email.text = EMAIL val password = device.findObject(By.res(PACKAGE, "password")) email.text = PASSWORD val login = device.findObject(By.res(PACKAGE, "login")) if (login != null) { ... }

Slide 19

Slide 19 text

APPIUM SUPPORT MANY LANGUAGES We can use different programming languages to create tests using the Apium framework. like Java, Python, Ruby, C#, etc. INTERACTION WITH ANY APP Appium framework can interact with any application installed on the device/ emulator. INTERACTION WITH OS, DEVICE Appium framework can interact with OS features and device buttons, screen rotation, etc. val email = driver.findElement(By.id("email")) email.value = EMAIL val password = driver.findElement(By.id("password")) password.value = PASSWORD val login = driver.findElement(By.id("login")) login.isDisplayed()

Slide 20

Slide 20 text

ESPRESSO UI AUTOMATOR APPIUM No SUPPORT OF RESOURCES TEST CASES TYPE EXECUTION TIME Yes Gray box 0.946 s No Black box 8.743 s Black box 12.154 s

Slide 21

Slide 21 text

TYPES OF UI TESTS

Slide 22

Slide 22 text

END-TO-END TESTS FUNCTIONAL TESTS COMPONENT TESTS VIEW COMPARISON TESTS

Slide 23

Slide 23 text

END-TO-END TESTS SCENARIO: Splash Screen 1. Login to application 2. Analyse coffee drink screen 3. Open Profile screen 4. Open Menu 5. Logout from application Open Login screen

Slide 24

Slide 24 text

END-TO-END TESTS INTERACTION WITH SERVER Interaction with production server. ENTRY POINT Similar to entry point of app.

Slide 25

Slide 25 text

INTERACTION WITH SERVER Interaction with the non-production server. NAVIGATION Usually, navigation is not needed. ENTRY POINT We can start test from required Activity/ Fragment. FUNCTIONAL TESTS

Slide 26

Slide 26 text

COMPONENT TESTS SHORT EXECUTION TIME Usually, we have no interaction with the server, no navigation, etc. APP INDEPENDANT TESTS Test cases verify the rendering of the view and usually use fake data.

Slide 27

Slide 27 text

DEVICE DEPENDENT Test cases based on screenshots; different devices can have other screen resolutions. MIGRATION REQURE SCRIPTS We need to update all reference screenshots from one screen resolution to another one. VIEW COMPARISON TESTS

Slide 28

Slide 28 text

END TO END (E2E) TESTS FUNCTIONAL TESTS ENTRY POINT Similar to entry point of app. APP VERIFICATION Slow verification of application. NAVIGATION Navigate to required screen. SERVER INTERACTION Interaction with production server. ENTRY POINT Start required screen of the app. SERVER INTERACTION Usually interaction with non- production server. NAVIGATION Usually no navigation. UI VERIFICATION Fast verification of components or screens.

Slide 29

Slide 29 text

EFFICIENCY READABILITY TIME EFFICIENCY COMPLEXITY

Slide 30

Slide 30 text

READABILITY DIFFERENT APPROACHES TIME EFFICIENCY COMPLEXITY

Slide 31

Slide 31 text

READABILITY

Slide 32

Slide 32 text

ESPRESSO // Enter email onView(withId(R.id.email)) .perform(replaceText(EMAIL)) // Enter password onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) // Press login button onView(withId(R.id.login)) .perform(click())

Slide 33

Slide 33 text

ESPRESSO onView(withId(R.id.emailEditText)) .perform(replaceText(email)) onView(withId(R.id.passwordEditText)) .perform(replaceText(password), closeSoftKeyboard()) onView(withId(R.id.loginButton)) .perform(click()) val progressBarIR = ViewVisibilityIdlingResource(...) IdlingRegistry.getInstance().register(progressBarIR) onView(withId(R.id.recyclerView)) .check(matches(withItemCount(13))) IdlingRegistry.getInstance().unregister(progressBarIR) onView(withId(R.id.navigation_profile)) .perform(click()) ...

Slide 34

Slide 34 text

ESPRESSO // Login screen onView(withId(R.id.emailEditText)) .perform(replaceText(email)) onView(withId(R.id.passwordEditText)) .perform(replaceText(password), closeSoftKeyboard()) onView(withId(R.id.loginButton)) .perform(click()) // Home screen val progressBarIR = ViewVisibilityIdlingResource(...) IdlingRegistry.getInstance().register(progressBarIR) onView(withId(R.id.recyclerView)) .check(matches(withItemCount(13))) IdlingRegistry.getInstance().unregister(progressBarIR) onView(withId(R.id.navigation_profile)) .perform(click()) ...

Slide 35

Slide 35 text

onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed()))

Slide 36

Slide 36 text

onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed())) loginScreen { login(EMAIL, PASSWORD) }

Slide 37

Slide 37 text

onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed())) loginScreen { login(EMAIL, PASSWORD) } open class BaseTestRobot { fun enterText(viewId: Int, text: String) { onView(withId(viewId)) .perform(replaceText(text)) } fun clickOnView(viewId: Int) { onView(withId(viewId)) .perform(click()) } }

Slide 38

Slide 38 text

onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed())) loginScreen { login(EMAIL, PASSWORD) } open class BaseTestRobot { fun enterText(viewId: Int, text: String) { onView(withId(viewId)) .perform(replaceText(text)) } fun clickOnView(viewId: Int) { onView(withId(viewId)) .perform(click()) } } class LoginScreenRobot : BaseTestRobot() { fun login(email: String, password: String) { enterText(R.id.email, email) enterText(R.id.password, password) clickOnView(R.id.loginButton) } }

Slide 39

Slide 39 text

onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed())) loginScreen { login(EMAIL, PASSWORD) } open class BaseTestRobot { fun enterText(viewId: Int, text: String) { onView(withId(viewId)) .perform(replaceText(text)) } fun clickOnView(viewId: Int) { onView(withId(viewId)) .perform(click()) } } class LoginScreenRobot : BaseTestRobot() { fun login(email: String, password: String) { enterText(R.id.email, email) enterText(R.id.password, password) clickOnView(R.id.loginButton) } } fun loginScreen(func: LoginScreenRobot.() -> Unit) = LoginScreenRobot().apply { func() }

Slide 40

Slide 40 text

BASIC OPERATIONS SCREEN ROBOTS TEST CASES @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun shouldBeDisplayedEmptyEmailErrorsWhenValueIsEmpty() { loginScreen { enterEmail(EMPTY_VALUE) emptyEmailErrorDisplayed() } } @Test fun shouldBeDisplayedEmailErrorsWhenValueIsNotEmail() { loginScreen { enterEmail(INCORRECT_EMAIL) incorrectEmailErrorDisplayed() } } @Test fun shouldBeDisplayedEmptyPasswordErrorsWhenValueIsEmpty() { loginScreen { enterPassword(EMPTY_VALUE) emptyPasswordErrorDisplayed() } } ... } class LoginScreenRobot : BaseTestRobot() { fun enterEmail(email: String) = enterText(R.id.email, email) fun enterPassword(email: String) = enterText(R.id.password, password) ... } open class BaseTestRobot { fun enterText(viewId: Int, text: String) { onView(withId(viewId)) .perform(replaceText(text)) } fun clickOnView(viewId: Int) { onView(withId(viewId)) .perform(click()) } ... } ESPRESSO

Slide 41

Slide 41 text

BASIC OPERATIONS SCREEN ROBOTS TEST CASES @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun shouldBeDisplayedEmptyEmailErrorsWhenValueIsEmpty() { loginScreen { enterEmail(EMPTY_VALUE) emptyEmailErrorDisplayed() } } @Test fun shouldBeDisplayedEmailErrorsWhenValueIsNotEmail() { loginScreen { enterEmail(INCORRECT_EMAIL) incorrectEmailErrorDisplayed() } } @Test fun shouldBeDisplayedEmptyPasswordErrorsWhenValueIsEmpty() { loginScreen { enterPassword(EMPTY_VALUE) emptyPasswordErrorDisplayed() } } ... } class LoginScreenRobot : BaseTestRobot() { fun enterEmail(email: String) = enterText(R.id.email, email) fun enterPassword(email: String) = enterText(R.id.password, password) ... } open class BaseTestRobot { fun enterText(viewId: Int, text: String) { val view = device.findObject(By.res(resId(viewId))) view.text = text } fun clickOnView(viewId: Int) { device.findObject(By.res(resIf(viewId))) .click() } ... } UI AUTOMATOR

Slide 42

Slide 42 text

loginScreen { enterEmail(EMAIL) enterPassword(PASSWORD) pressLogin() } homeScreen { waitCoffeeDrinks() pressProfile() } profileScreen { waitUserData() hasEmail(EMAIL) hasUserName(USER_NAME) openMenu() pressLogout() } loginScreen { isOpen() } loginScreen { login(EMAIL, PASSWORD) } homeScreen { navigateToProfile() } profileScreen { hasEmail(EMAIL) hasUserName(USER_NAME) logout() } loginScreen { isOpen() }

Slide 43

Slide 43 text

DIFFERENT APPROACHES

Slide 44

Slide 44 text

DEBUG SERVER APPLICABLE TO E2E TESTS We can update the URL constant during compilation APK for instrumentation test cases. MAKE TESTS MORE STABLE We can have stable data on the debug server. DATA SYNCHRONIZATION We need to synchronize data between the production and debug server. Production Debug

Slide 45

Slide 45 text

SHORT EXECUTION TIME Interaction with files and databases is faster than using UI for data changes. PRE-POPULATING DATA Server Database

Slide 46

Slide 46 text

MOCK API RESPONSES MAKE TESTS MORE STABLE We can use predefined fake responses instead of using a production server. NO INTERACTION WITH SERVER Responses from the production server can differ from predefined data. DATA SYNCHRONIZATION We need to synchronize predefined responses with responses from the production server. Mock-Server

Slide 47

Slide 47 text

FLEXIBILITY We can define what we want to test and which layers should be mocked. SHORT EXECUTION TIME Short execution time can be achieved if we mock top layers of the application. MOCK APP LAYERS APP DOMAIN DATA

Slide 48

Slide 48 text

Activities Fragments

Slide 49

Slide 49 text

Fragments @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun superImportantTest() { ... launchFragmentInContainer() ... } }

Slide 50

Slide 50 text

TIME EFFICIENCY

Slide 51

Slide 51 text

END TO END (E2E) TESTS FUNCTIONAL TESTS ENTRY POINT Similar to entry point of app. APP VERIFICATION Slow verification of application. NAVIGATION Navigate to required screen. SERVER INTERACTION Interaction with production server. ENTRY POINT Start required screen of the app. SERVER INTERACTION Usually interaction with non- production server. NAVIGATION Usually no navigation. UI VERIFICATION Fast verification of components or screens.

Slide 52

Slide 52 text

END-TO-END TESTS FUNCTIONAL TESTS

Slide 53

Slide 53 text

COMPLEXITY

Slide 54

Slide 54 text

FRAMEWORKS DOMAIN SPECIFIC LANGUAGE TEST INFRASRUCTURE KNOWN APPROACHES

Slide 55

Slide 55 text

UI TESTING FRAMEWORKS APPIUM ESPRESSO KAKAO BARISTA KASPERSSO UI AUTOMATOR

Slide 56

Slide 56 text

APPIUM UI AUTOMATOR ESPRESSO KAKAO KASPRESSO BARISTA KAUTOMATOR

Slide 57

Slide 57 text

BARISTA LESS BOILERPLATE CODE Barista API is removing a lot of boilerplate code with its own abstraction over Espresso. USEFUL TEST RULES Barista library provides JUnit rules for clearing data on device/emulation, like ClearFilesRule, ClearPreferencesRule, etc. HANDLING FLAKY TESTS Barista library provides annotations for handling flaky tests, like AllowFlaky, Repeat. writeTo(R.id.email, EMAIL) writeTo(R.id.password, PASSWORD) clickOn(R.id.login) assertDisplayed(R.id.name, USER_NAME) assertDisplayed(R.id.email, EMAIL)

Slide 58

Slide 58 text

PREDEFINED DSL Kakao framework provides an abstraction on top of the Espresso framework. INTERCEPTORS MECHANISM An interceptor allows you to inject other logic between Kakao → Espresso call chain. KAKAO onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() }

Slide 59

Slide 59 text

KASPRESSO BASED ON KAKAO We have access to all features from Kakao and Espresso frameworks. ADVANCED REPORTING SYSTEM We can generate human-readable reports with the Kaspresso framework. HANDLING FLAKY TESTS Kaspresso support functions for handling flaky operation based on interceptor mechanism, like flakySafely. beforeTest { activityTestRule.launchActivity(intent) }.afterTest { ... }.run { onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() } }

Slide 60

Slide 60 text

BEST PRACTICES NAMING QUALITY FLAKY TESTS

Slide 61

Slide 61 text

LEARN YOUR TOOLS

Slide 62

Slide 62 text

PAY ATTENTION TO NAMING

Slide 63

Slide 63 text

NO "SLEEP" IN TESTS

Slide 64

Slide 64 text

ALL TEST CASES SHOULD BE INDEPENDENT

Slide 65

Slide 65 text

MAKE AN ABSTRACTION OVER THE FRAMEWORK

Slide 66

Slide 66 text

FOLLOW THE "NO FLAKY TESTS" POLICY

Slide 67

Slide 67 text

DON’T RELY ONLY ON UI TEST AUTOMATION

Slide 68

Slide 68 text

THANK YOU FOR LISTENING! @alex_zhukovich https://alexzh.com/ alex-zhukovich