Slide 1

Slide 1 text

Writing Robust Android UI Tests @Alex_Zhukovich

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Non-UI testing UI testing Stress testing View Comparison testing

Slide 4

Slide 4 text

Android tests Local Instrumentation

Slide 5

Slide 5 text

Android tests Local Instrumentation Non-UI UI* UI Non-UI

Slide 6

Slide 6 text

adb shell am instrument Instrumentation >_ Test app Dev app

Slide 7

Slide 7 text

https://github.com/AlexZhukovich/ImBarista-App

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

@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()); } ... } Espresso Test Recorder

Slide 11

Slide 11 text

val textInputEditText = onView( allOf( withId(R.id.emailEditText), childAtPosition( childAtPosition( withId(R.id.emailInputLayout), 0 ), 0 ) ) ) textInputEditText.perform(scrollTo(), replaceText("[email protected]"), closeSoftKeyboard()) val textInputEditText2 = onView( allOf( withId(R.id.passwordEditText), childAtPosition( childAtPosition( withId(R.id.passwordInputLayout), 0 ), 0 ) ) ) textInputEditText2.perform(scrollTo(), replaceText("test"), closeSoftKeyboard()) val materialButton = onView( allOf( withId(R.id.loginButton), withText("Login"), childAtPosition( childAtPosition( withId(R.id.scrollView2), 0 ), 4 ) ) ) materialButton.perform(scrollTo(), click()) onView(withId(R.id.emailEditText)) .perform(replaceText("[email protected]")) onView(withId(R.id.passwordEditText)) .perform(replaceText("test")) onView(withId(R.id.loginButton)) .perform(click())

Slide 12

Slide 12 text

Changes in app Network speed Unreadability Test data

Slide 13

Slide 13 text

Layout Inspector UI Automator Viewer

Slide 14

Slide 14 text

@Test fun shouldDisplayErrorWhenPasswordIsBlank() { val email = "[email protected]" val password = "" onView(withId(R.id.emailEditText)) .perform(replaceText(email)) onView(withId(R.id.passwordEditText)) .perform(replaceText(password)) onView(withId(R.id.loginButton)) .perform(click()) onView(withText(R.string.password_is_blank)) .check(matches(isDisplayed())) }

Slide 15

Slide 15 text

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 16

Slide 16 text

// -- LOGIN -- 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 17

Slide 17 text

Domain Specific Language

Slide 18

Slide 18 text

Scenario: 1. Login to application

Slide 19

Slide 19 text

Scenario: 1. Login to application 2. Open Profile screen 3. Check name and email

Slide 20

Slide 20 text

Scenario: 1. Login to application 2. Open Profile screen 3. Check name and email 4. Open menu 5. Logout from application

Slide 21

Slide 21 text

// LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click())

Slide 22

Slide 22 text

// LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click()) 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 23

Slide 23 text

// LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click()) 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 24

Slide 24 text

// LOGIN onView(withId(R.id.email)) .perform(replaceText(EMAIL)) onView(withId(R.id.password)) .perform(replaceText(PASSWORD)) onView(withId(R.id.login)) .perform(click()) 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 25

Slide 25 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 26

Slide 26 text

Test case Screen Robot Basic operations

Slide 27

Slide 27 text

Readability Reusability Building complexity Framework independant

Slide 28

Slide 28 text

Approaches and tools

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

Clear data

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Mock Fake

Slide 33

Slide 33 text

End to End tests Functional tests 1 1 Entry point Start test from the main screen 2 Interaction with server Verification interaction with server 3 Navigation Verification navigation between screens Entry point Start application from any screen 2 Interaction with server Usually no interaction with prod server 3 Navigation Usually no navigation between screen Navigation between neighbor screens 4 UI verification Fast verification of UI components 4 App verification Slow verification of entire product

Slide 34

Slide 34 text

Espresso UiAutomator Appium onView(withId(R.id.email)) .perform(replaceText(EMAIL) onView(withId(R.id.password)) .perform(replaceText(PASSWORD) onView(withId(R.id.login)) .check(matchers(isDisplayed())) Synchronization IdlingResources Run any Activity

Slide 35

Slide 35 text

Espresso UiAutomator Appium 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) { ... } Interact with OS Interact with any application

Slide 36

Slide 36 text

Espresso UiAutomator Appium 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() Interact with OS Interact with any application Support many languages

Slide 37

Slide 37 text

Espresso UiAutomator Appium Support of Android resources Yes No No

Slide 38

Slide 38 text

Espresso UiAutomator Appium Support of Android resources Yes No No Test cases type GrayBox BlackBox BlackBox

Slide 39

Slide 39 text

Espresso UiAutomator Appium Support of Android resources Yes No No Test cases type GrayBox BlackBox BlackBox Execution time 0.976 s 8.743 s 12.154 s

Slide 40

Slide 40 text

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) Kakao Barista Kaspresso

Slide 41

Slide 41 text

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) @Rule ClearPreferencesRule @Rule ClearDatabaseRule @Rule ClearFilesRule Kakao Barista Kaspresso

Slide 42

Slide 42 text

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) @Rule ClearPreferencesRule @Rule ClearDatabaseRule @Rule ClearFilesRule @Test @AllowFlaky(attempts = 5) @Repeat(times = 5) fun flaky_test() { ... } Kakao Barista Kaspresso

Slide 43

Slide 43 text

@Test fun should_verify_account_information() { val toolbarVisibilityIR = ViewVisibilityIdlingResource(R.id.toolbar, View.VISIBLE) IdlingRegistry.getInstance().register(toolbarVisibilityIR) assertDisplayed(R.id.toolbar) IdlingRegistry.getInstance().unregister(toolbarVisibilityIR) // LOGIN writeTo(R.id.emailEditText, EMAIL) writeTo(R.id.passwordEditText, PASSWORD) closeKeyboard() clickOn(R.id.loginButton) // NAVIGATE TO PROFILE val progressBarIR = ViewVisibilityIdlingResource(R.id.progressBar, View.GONE) IdlingRegistry.getInstance().register(progressBarIR) assertDisplayed(R.id.recyclerView) IdlingRegistry.getInstance().unregister(progressBarIR) clickOn(R.id.navigation_profile) // VERIFY PROFILE INFO val progressProfileIR = ViewVisibilityIdlingResource(R.id.progressBar, View.GONE) IdlingRegistry.getInstance().register(progressProfileIR) assertDisplayed(R.id.nameTextView, USER_NAME) assertDisplayed(R.id.emailTextView, EMAIL) IdlingRegistry.getInstance().unregister(progressProfileIR) // LOGOUT clickMenu(R.id.logout_action) // LOGIN SCREEN intended(hasComponent(LoginActivity::class.java.name)) } Kakao Barista Kaspresso

Slide 44

Slide 44 text

Kakao Barista Kaspresso onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() }

Slide 45

Slide 45 text

onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() } Interceptors Kakao Barista Kaspresso

Slide 46

Slide 46 text

@Test fun should_verify_account_information() { onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() } onScreen { waitCoffeeDrinkList() profileMenuItem.click() } onScreen { waitForProfileInfo() name.hasText(USER_NAME) email.hasText(EMAIL) openMenu() logout.click() } onScreen { loginScreenIntent.intended() } } Kakao Barista Kaspresso

Slide 47

Slide 47 text

onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() } Include KAutomator Injecting data DSL Interaction with Device Advanced reporting system Handling flaky tests Kakao Barista Kaspresso

Slide 48

Slide 48 text

Kakao @Test fun should_verify_account_information() = beforeTest { activityTestRule.launchActivity(intent) }.afterTest { ... }.run { onScreen { email.replaceText(EMAIL) password { replaceText(PASSWORD) closeSoftKeyboard() } login.click() } onScreen { waitCoffeeDrinkList() profileMenuItem.click() } onScreen { waitForProfileInfo() name.hasText(USER_NAME) email.hasText(EMAIL) openMenu() logout.click() } onScreen { loginScreenIntent.intended() } } Barista Kaspresso

Slide 49

Slide 49 text

Pre-populating WireMock RestMock MockWebServer Fake Mock

Slide 50

Slide 50 text

Activity Fragment

Slide 51

Slide 51 text

Activity @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun superImportantTest() { ... ActivityScenario.launch( LoginFragment::class.java ) ... } } Fragment @RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun superImportantTest() { ... launchFragmentInContainer() ... } }

Slide 52

Slide 52 text

End To End tests Functional tests

Slide 53

Slide 53 text

Shared tests

Slide 54

Slide 54 text

Local tests Instrumentation tests Shared tests

Slide 55

Slide 55 text

android { ... sourceSets { androidTest { java.srcDirs += "src/sharedTest/java" } test { java.srcDirs += "src/sharedTest/java" } } } androidTest sharedTest test

Slide 56

Slide 56 text

How to start

Slide 57

Slide 57 text

App flow Priorities Analytical data

Slide 58

Slide 58 text

App flow Priorities Analytical data

Slide 59

Slide 59 text

App flow Priorities Analytical data

Slide 60

Slide 60 text

Flaky tests

Slide 61

Slide 61 text

Testing approaches Tools Parallel execution Test data

Slide 62

Slide 62 text

FAILED TESTS FLAKY TESTS

Slide 63

Slide 63 text

Android Test Orchestrator @Rule ClearPreferencesRule ClearDatabaseRule ClearFilesRule Interceptors @AllowFlaky @

Slide 64

Slide 64 text

#ExploreMore Android UI testing alexzh.com @AlexZhukovich