Slide 1

Slide 1 text

@alex_zhukovich https://alexzh.com/ Modern Android UI Testing Workshop

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Android Testing World

Slide 4

Slide 4 text

ANDROID TESTS LOCAL INSTRUMENTATION

Slide 5

Slide 5 text

ANDROID TESTS LOCAL INSTRUMENTATION UI* Non-UI

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

app.apk tests.apk BUILD APKs

Slide 8

Slide 8 text

app.apk tests.apk >_ adb shell am instrument

Slide 9

Slide 9 text

app.apk tests.apk >_ adb shell am instrument

Slide 10

Slide 10 text

ACTIVITY FRAGMENT

Slide 11

Slide 11 text

ACTIVITY FRAGMENT COMPOSABLE FUNCTION

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

launchActivity launchFragmentInContainer ComposeContentTestRule ActivityScenario.launch( MainActivity::class.java )

Slide 15

Slide 15 text

launchActivity launchFragmentInContainer ComposeContentTestRule val activityOptions = bundleOf( Pair("param-1", 1), Pair("param-2", 2), Pair("param-3", 3), ) val scenario = ActivityScenario.launch( MainActivity::class.java, activityOptions ) scenario.moveToState(Lifecycle.State.STARTED) ActivityScenario.launch( MainActivity::class.java )

Slide 16

Slide 16 text

launchActivity launchFragmentInContainer ComposeContentTestRule val scenario = launchFragmentInContainer( fragmentArgs: Bundle? = null, @StyleRes themeResId: Int = 
 R.style.FragmentScenarioEmptyFragmentActivityTheme, initialState: Lifecycle.State = 
 Lifecycle.State.RESUMED, factory: FragmentFactory? = null ) scenario.moveToState(Lifecycle.State.STARTED) debugImplementation "androidx.fragment:fragment-testing:1.4.0"

Slide 17

Slide 17 text

launchActivity launchFragmentInContainer ComposeContentTestRule Activity Fragment

Slide 18

Slide 18 text

launchActivity launchFragmentInContainer ComposeContentTestRule androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity Activity Fragment Compilation Added to AndroidManifest

Slide 19

Slide 19 text

launchActivity launchFragmentInContainer ComposeContentTestRule @get:Rule val composeTestRule = createComposeRule() composeTestRule.apply { setContent { LoginScreen() } onNodeWithTag("email") .performTextInput("[email protected]") onNodeWithTag("password") .performTextInput("password") onNodeWithTag("login") .performClick() ... } debugImplementation "androidx.compose.ui:ui-test-manifest:1:0:5"

Slide 20

Slide 20 text

launchActivity launchFragmentInContainer ComposeContentTestRule Component Activity @Composable

Slide 21

Slide 21 text

launchActivity launchFragmentInContainer ComposeContentTestRule androidx.activity.ComponentActivity Component Activity @Composable Compilation Added to AndroidManifest

Slide 22

Slide 22 text

Testing Android Views

Slide 23

Slide 23 text

COMPONENT HAS ID PROPERTIES REAL CLOCK

Slide 24

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

Slide 25 text

Testing Composable code

Slide 26

Slide 26 text

NO ID 
 OF COMPONENT SEMANTIC PROPERTIES VIRTUAL CLOCK

Slide 27

Slide 27 text

class SubscribeBoxTest { @get:Rule val composeTestRule = createComposeRule() @Test fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() { composeTestRule.apply { setContent { SubscribeBox() } onNode(hasTestTag("subscribe_email")) .performTextInput("[email protected]") onNode(hasText("SUBSCRIBE")) .performClick() onNode(hasText("You successfully subscribed")) .assertIsDisplayed() } } }

Slide 28

Slide 28 text

class SubscribeBoxTest { @get:Rule val composeTestRule = createComposeRule() @Test fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() { composeTestRule.apply { setContent { SubscribeBox() } onNode(hasTestTag("subscribe_email")) .performTextInput("[email protected]") onNode(hasText("SUBSCRIBE")) .performClick() onNode(hasText("You successfully subscribed")) .assertIsDisplayed() } } } Finder Matcher

Slide 29

Slide 29 text

class SubscribeBoxTest { @get:Rule val composeTestRule = createComposeRule() @Test fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() { composeTestRule.apply { setContent { SubscribeBox() } onNode(hasTestTag("subscribe_email")) .performTextInput("[email protected]") onNode(hasText("SUBSCRIBE")) .performClick() onNode(hasText("You successfully subscribed")) .assertIsDisplayed() } } } Finder Matcher Action

Slide 30

Slide 30 text

class SubscribeBoxTest { @get:Rule val composeTestRule = createComposeRule() @Test fun shouldDisplaySnackbar_whenEmailIsEnteredAndSubscribeClicked() { composeTestRule.apply { setContent { SubscribeBox() } onNode(hasTestTag("subscribe_email")) .performTextInput("[email protected]") onNode(hasText("SUBSCRIBE")) .performClick() onNode(hasText("You successfully subscribed")) .assertIsDisplayed() } } } Finder Matcher Action Assertion

Slide 31

Slide 31 text

MERGED NODE TREE UNMERGED NODE TREE Printing with useUnmergedTree = ‘false’ Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px Role = 'Button' Focused = 'false' ContentDescription = '[Schedule a task]' Text = '[Schedule a task]' Actions = [OnClick, GetTextLayoutResult] MergeDescendants = 'true' Printing with useUnmergedTree = ‘true’ Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px Role = 'Button' Focused = 'false' Actions = [OnClick] MergeDescendants = 'true' |-Node #4 at (l=321.0, t=1006.0, r=395.0, b=1080.0)px | ContentDescription = '[Schedule a task]' | Role = 'Image' |-Node #5 at (l=420.0, t=1014.0, r=785.0, b=1072.0)px Text = '[Schedule a task]' Actions = [GetTextLayoutResult]

Slide 32

Slide 32 text

MERGED NODE TREE UNMERGED NODE TREE Printing with useUnmergedTree = ‘true’ Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px Role = 'Button' Focused = 'false' Actions = [OnClick] MergeDescendants = 'true' |-Node #4 at (l=321.0, t=1006.0, r=395.0, b=1080.0)px | ContentDescription = '[Schedule a task]' | Role = 'Image' |-Node #5 at (l=420.0, t=1014.0, r=785.0, b=1072.0)px Text = '[Schedule a task]' Actions = [GetTextLayoutResult] Printing with useUnmergedTree = ‘false’ Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px Role = 'Button' Focused = 'false' ContentDescription = '[Schedule a task]' Text = '[Schedule a task]' Actions = [OnClick, GetTextLayoutResult] MergeDescendants = 'true' composeTestRule .onRoot(useUnmergedTree = false) 
 .printToLog("MERGED") composeTestRule .onRoot(useUnmergedTree = true) .printToLog("UNMERGED")

Slide 33

Slide 33 text

MERGED NODE TREE UNMERGED NODE TREE onNode(hasText("Schedule a task"), useUnmergedTree = true) .assertHasClickAction() Error onNode(hasText("Schedule a task")) .assertHasClickAction() useUnmergedTree = false Printing with useUnmergedTree = ‘true’ Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px Role = 'Button' Focused = 'false' Actions = [OnClick] MergeDescendants = 'true' |-Node #4 at (l=321.0, t=1006.0, r=395.0, b=1080.0)px | ContentDescription = '[Schedule a task]' | Role = 'Image' |-Node #5 at (l=420.0, t=1014.0, r=785.0, b=1072.0)px Text = '[Schedule a task]' Actions = [GetTextLayoutResult] Printing with useUnmergedTree = ‘false’ Node #1 at (l=0.0, t=74.0, r=1080.0, b=2012.0)px |-Node #2 at (l=247.0, t=956.0, r=834.0, b=1130.0)px Role = 'Button' Focused = 'false' ContentDescription = '[Schedule a task]' Text = '[Schedule a task]' Actions = [OnClick, GetTextLayoutResult] MergeDescendants = 'true' composeTestRule .onRoot(useUnmergedTree = false) 
 .printToLog("MERGED") composeTestRule .onRoot(useUnmergedTree = true) .printToLog("UNMERGED")

Slide 34

Slide 34 text

onAllNodesWithContentDescription("Happy")[0] .assert(hasText(“08:30")) onAllNodes(hasContentDescription("Happy"))[1] .assert(hasText("19:45")) onNodeWithText("Add") .performClick()

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

@Preview @Composable fun Preview_MultipleRadioButtons() { val selectedValue = remember { mutableStateOf("") } val isSelectedItem: (String) -> Boolean = { selectedValue.value == it } val onChangeState: (String) -> Unit = { selectedValue.value = it } val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5") Column(Modifier.padding(8.dp)) { Text(text = "Selected value: ${selectedValue.value.ifEmpty { "NONE" }}") items.forEach { item -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.selectable( selected = isSelectedItem(item), onClick = { onChangeState(item) }, role = Role.RadioButton ).padding(8.dp) ) { RadioButton( selected = isSelectedItem(item), onClick = null ) Text( text = item, modifier = Modifier.fillMaxWidth() ) } } } }

Slide 37

Slide 37 text

@Preview @Composable fun Preview_MultipleRadioButtons() { val selectedValue = remember { mutableStateOf("") } val isSelectedItem: (String) -> Boolean = { selectedValue.value == it } val onChangeState: (String) -> Unit = { selectedValue.value = it } val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5") Column(Modifier.padding(8.dp)) { Text(text = "Selected value: ${selectedValue.value.ifEmpty { "NONE" }}") items.forEach { item -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.selectable( selected = isSelectedItem(item), onClick = { onChangeState(item) }, role = Role.RadioButton ).padding(8.dp) ) { RadioButton( selected = isSelectedItem(item), onClick = null ) Text( text = item, modifier = Modifier.fillMaxWidth() ) } } } }

Slide 38

Slide 38 text

@ExperimentalFoundationApi class RadioButtonTest { @get:Rule val composeTestRule = createComposeRule() @Test fun firstItemSelectedByDefault_whenThirdItemIsSelected_thenSelectedValueDisplayed() { composeTestRule.apply { setContent { Demo_RadioButton () } onNodeWithRoleAndText(Role.RadioButton, "Item 1") .assertIsSelected() onNodeWithText("Item 3") .performClick() onNodeWithRoleAndText(Role.RadioButton, "Item 3") .assertIsSelected() onNodeWithText("Selected value: Item 3") .assertIsDisplayed() } } private fun withRole(role: Role): SemanticsMatcher { return SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { it.config.getOrNull(SemanticsProperties.Role) == role } } private fun SemanticsNodeInteractionsProvider.onNodeWithRoleAndText( role: Role, text: String ) = onNode( withRole(role) .and(isSelectable()) .and(isEnabled()) .and(hasText(text)) ) }

Slide 39

Slide 39 text

@ExperimentalFoundationApi class RadioButtonTest { @get:Rule val composeTestRule = createComposeRule() @Test fun firstItemSelectedByDefault_whenThirdItemIsSelected_thenSelectedValueDisplayed() { composeTestRule.apply { setContent { Demo_RadioButton () } onNodeWithRoleAndText(Role.RadioButton, "Item 1") .assertIsSelected() onNodeWithText("Item 3") .performClick() onNodeWithRoleAndText(Role.RadioButton, "Item 3") .assertIsSelected() onNodeWithText("Selected value: Item 3") .assertIsDisplayed() } } private fun withRole(role: Role): SemanticsMatcher { return SemanticsMatcher("${SemanticsProperties.Role.name} contains '$role'") { it.config.getOrNull(SemanticsProperties.Role) == role } } private fun SemanticsNodeInteractionsProvider.onNodeWithRoleAndText( role: Role, text: String ) = onNode( withRole(role) .and(isSelectable()) .and(isEnabled()) .and(hasText(text)) ) }

Slide 40

Slide 40 text

Application

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

Presentation (UI) Data layer *Repository SessionManager SharedPreferences SharedPreferences Database

Slide 43

Slide 43 text

End-To-End tests

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

Database

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

Session Manager Shared Preferences

Slide 48

Slide 48 text

Functional tests

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

launchFragmentInContainer( themeResId = R.style.Theme_MoodTracker ) composeTestRule.apply { ... }

Slide 51

Slide 51 text

Server Database

Slide 52

Slide 52 text

Database Query Dependencies UI interaction

Slide 53

Slide 53 text

Prod server Dev server Mock/Fake

Slide 54

Slide 54 text

Prod server Dev server DATA SYNCHRONIZATION We need to synchronize data between the production and dev servers. INTERACTION WITH THE SERVER We make requests to the production server. The connection can require certificates, VPN, etc. PRODUCTION BACK-END We always use the latest available production environment.

Slide 55

Slide 55 text

Mock/Fake DATA SYNCHRONIZATION We need to synchronize predefined responses with responses from the production server. NO INTERACTION WITH THE SERVER Responses from the production server can differ from the predefined data. We can use predefined fake responses instead of calling the production server. MAKE TESTS MORE STABLE

Slide 56

Slide 56 text

Mock/Fake

Slide 57

Slide 57 text

Mock/Fake

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Screenshot tests

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

What can we check with screenshot tests?

Slide 62

Slide 62 text

@Composable fun LoadingButton( onClick: () -> Unit, text: String, modifier: Modifier = Modifier, shape: Shape = MaterialTheme.shapes.small, enabled: Boolean = true, isLoading: Boolean = false, loadingIndicatorColor: Color = MaterialTheme.colorScheme.surface ) { ... }

Slide 63

Slide 63 text

AndroidUiTestingUtils https://github.com/sergio-sastre/AndroidUiTestingUtils Locale Light/Dark mode Display size, screen orientation Font size

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

@Test fun settingsScreen_dark_defaultState() { val fragmentScenario = FragmentScenarioConfigurator .setUiMode(UiMode.NIGHT) .setTheme(R.style.Theme_MoodTracker) .launchInContainer( SettingsFragment::class.java ) compareScreenshot( fragment = fragmentScenario.waitForFragment(), name = "settingsScreen_dark" ) fragmentScenario.close() } @Test fun settingsScreen_light_defaultState() { val fragmentScenario = FragmentScenarioConfigurator .setUiMode(UiMode.DAY) .setTheme(R.style.Theme_MoodTracker) .launchInContainer( SettingsFragment::class.java ) compareScreenshot( fragment = fragmentScenario.waitForFragment(), name = "settingsScreen_light" ) fragmentScenario.close() }

Slide 69

Slide 69 text

What to test?

Slide 70

Slide 70 text

PIXEL PERFECTNESS INTERACTION TESTING UI COMPONENTS 
 (DESIGN SYSTEM) INTERACTION WITH SCREEN(S) SUPPORT MULTIPLE LANGUAGES SUPPORT DIFFERENT 
 COLOR THEMES SUPPORT ACCESSIBILITY OPTIONS 
 (FONT SIZE, SCREEN SIZE, ETC) SUPPORT LTR & RTL PERMISSION TESTING YES YES YES YES YES YES YES YES

Slide 71

Slide 71 text

END-TO-END TESTS FUNCTIONAL TESTS SCREENSHOT TESTS

Slide 72

Slide 72 text

End To End testing 
 Maestro

Slide 73

Slide 73 text

No content

Slide 74

Slide 74 text

•add-emotional-state.yaml •login-test-user.yaml •time-format-flow.yaml TA SK e2e-flow-test

Slide 75

Slide 75 text

appId: com.alexzh.moodtracker --- - launchApp: clearState: true - tapOn: "Add" - tapOn: "Excited" - tapOn: "Select a time" - tapOn: "8" - tapOn: "15" - tapOn: "OK" - tapOn: "Gaming" - tapOn: "Save" - assertVisible: "08:15" - assertVisible: "Gaming" @Test fun displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen() { ActivityScenario.launch(HomeActivity::class.java) composableTestRule.apply { waitUntil { onAllNodesWithText("Emotions") .fetchSemanticsNodes().size == 1 } onNode(hasContentDescription("Add")) .performClick() waitUntil { onAllNodesWithContentDescription("Happy") .fetchSemanticsNodes().size == 1 } onNodeWithContentDescription("Happy") .performClick() onNodeWithText("Reading") .performClick() onNodeWithText("Gaming") .performClick() onNodeWithText("Note") .performTextInput("Test note") onNode(hasText("Save")) .performScrollTo() .performClick() waitUntil { onAllNodesWithText("Emotions") .fetchSemanticsNodes().size == 1 } onNode(withEmotionStateAndNote("Happy", "Test note")) .assert(hasAnyChild(hasText("Gaming"))) onNode(hasContentDescription("Happy")) .performSemanticsAction(SemanticsActions.OnClick) } }

Slide 76

Slide 76 text

appId: com.alexzh.moodtracker --- - launchApp: clearState: true - tapOn: "Add" - tapOn: "Excited" - tapOn: "Select a time" - tapOn: "8" - tapOn: "15" - tapOn: "OK" - tapOn: "Gaming" - tapOn: "Save" - assertVisible: "08:15" - assertVisible: "Gaming" launchApp() todayScreen { addEmotionalState() } addMoodScreen { selectEmotion("Happy") selectActivity("Reading", "Gaming") enterNote(note) save() } todayScreen { hasItem( "Happy", “Test note”, "Reading", "Gaming" ) openEmotionalItem("Happy") } addMoodScreen { delete() }

Slide 77

Slide 77 text

@Test fun displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen() { ActivityScenario.launch(HomeActivity::class.java) composableTestRule.apply { waitUntil { onAllNodesWithText("Emotions") .fetchSemanticsNodes().size == 1 } onNode(hasContentDescription("Add")) .performClick() waitUntil { onAllNodesWithContentDescription("Happy") .fetchSemanticsNodes().size == 1 } onNodeWithContentDescription("Happy") .performClick() onNodeWithText("Reading") .performClick() onNodeWithText("Gaming") .performClick() onNodeWithText("Note") .performTextInput("Test note") onNode(hasText("Save")) .performScrollTo() .performClick() waitUntil { onAllNodesWithText("Emotions") .fetchSemanticsNodes().size == 1 } onNode(withEmotionStateAndNote("Happy", "Test note")) .assert(hasAnyChild(hasText("Gaming"))) onNode(hasContentDescription("Happy")) .performSemanticsAction(SemanticsActions.OnClick) } } launchApp() todayScreen { addEmotionalState() } addMoodScreen { selectEmotion("Happy") selectActivity("Reading", "Gaming") enterNote(note) save() } todayScreen { hasItem( "Happy", note, "Reading", "Gaming" ) openEmotionalItem("Happy") } addMoodScreen { delete() }

Slide 78

Slide 78 text

Screenshot tests

Slide 79

Slide 79 text

No content

Slide 80

Slide 80 text

https://github.com/cashapp/paparazzi https://github.com/pedrovgs/Shot SHOT PAPARAZZI

Slide 81

Slide 81 text

SHOT PAPARAZZI https://github.com/cashapp/paparazzi https://github.com/pedrovgs/Shot DEVICE JVM

Slide 82

Slide 82 text

https://github.com/cashapp/paparazzi https://github.com/pedrovgs/Shot SHOT DEVICE PAPARAZZI JVM

Slide 83

Slide 83 text

Testing Components 
 in isolation

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

TA SK LoadingButtonColorModeScreenshotTest •loadingButton_light_defaultState •loadingButton_dark_defaultState •loadingButton_light_loadingState •loadingButton_dark_loadingState •loadingButton_loadingAnimation (LoadingButtonAnimationTest)

Slide 86

Slide 86 text

TA SK WeekCalendarColorModeScreenshotTest •weekCalendar_light_todayInSelectedDate •weekCalendar_dark_todayInSelectedDate •weekCalendar_light_todayInNotSelectedDate •weekCalendar_dark_todayInNotSelectedDate

Slide 87

Slide 87 text

Testing Screens 
 in isolation

Slide 88

Slide 88 text

TA SK SettingsScreenScreenshotTest •settingsScreen_light_defaultState •settingsScreen_dark_defaultState

Slide 89

Slide 89 text

Parameterized tests

Slide 90

Slide 90 text

TestParameterInjector https://github.com/google/TestParameterInjector Strings Enums Primitive Types

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

TA SK LoadingButtonParamScreenshotTest •loadingButton_customUiModeAndLoadingStateAndTitle

Slide 93

Slide 93 text

TA SK WeekCalendarParamScreenshotTest •weekCalendar_paramUiModeAndSelectedDate

Slide 94

Slide 94 text

TA SK SettingsScreenParamScreenshotTest •settingsScreen_customFontSizeAnUiMode

Slide 95

Slide 95 text

Functional testing

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

TA SK ProfileScreenTest •displayUserInfo_WhenUserIsLoggedIn •displayCreateAccountAndLoginOptions_WhenUserIsNotLoggedIn

Slide 98

Slide 98 text

TA SK LoginScreenTest •displayEmailIsTooShortError_whenEnteredEmailIsShorterThanFourSymbols •displayPasswordIsTooShortError_whenEnteredPasswordIsShorterThanFourSymbols

Slide 99

Slide 99 text

TA SK TodayScreenTest •displaySuccessWithSimpleItem_whenDataIsAvailable •displaySuccessWithMultipleItems_whenDataIsAvailable •displayEmptyState_whenDataIsNotAvailable

Slide 100

Slide 100 text

Readability

Slide 101

Slide 101 text

onNodeWithText("Login") .performClick() onNodeWithText("Email") .performTextInput("[email protected]") onNodeWithText("Password") .performTextInput("password") onNode(hasText("LOGIN")) .performClick() onNodeWithText("Alex") .assertIsDisplayed() onNodeWithText(“[email protected]") .assertIsDisplayed()

Slide 102

Slide 102 text

// Profile screen onNodeWithText("Login") .performClick() // Login screen onNodeWithText("Email") .performTextInput("[email protected]") onNodeWithText("Password") .performTextInput("password") onNode(hasText("LOGIN")) .performClick() // Profile screen onNodeWithText("Alex") .assertIsDisplayed() onNodeWithText("[email protected]") .assertIsDisplayed()

Slide 103

Slide 103 text

profileScreen(composeTestRule) { tapOnLogin() } loginScreen(composeTestRule) { login( email = "[email protected]", password = "password" ) } profileScreen(composeTestRule) { hasUserInfo( email = "[email protected]", name = "Alex" ) }

Slide 104

Slide 104 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 105

Slide 105 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 106

Slide 106 text

open class BaseTestRobot { @get:Rule val composeTestRule = createComposeRule() fun enterText(tag: String, text: String) { composeTestRule.onNodeWithTag(tag) .performTextInput(text) } fun clickOnView(tag: String) { composeTestRule.onNodeWithTag(tag) .performClick() } } @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() } } ... } BASIC OPERATIONS SCREEN ROBOTS TEST CASES COMPOSE UI TESTS class LoginScreenRobot : BaseTestRobot() { fun enterEmail(email: String) = enterText("email", email) fun enterPassword(email: String) = 
 enterText("password", password) ... }

Slide 107

Slide 107 text

TA SK TodayScreenE2ETestDsl •displayEmotion_WhenEmotionHistoryWasAddedViaAddMoodScreen

Slide 108

Slide 108 text

Flaky Tests

Slide 109

Slide 109 text

External dependencies Framework Test case execution Device and emulator

Slide 110

Slide 110 text

External dependencies - Network connection (VPN) - Network speed - Back-end

Slide 111

Slide 111 text

Framework - Framework issues - Toast, Snackbar, etc - Custom Views

Slide 112

Slide 112 text

Device and emulator - Performance - Noti fi cations - Device memory

Slide 113

Slide 113 text

Test case execution - Simulate User actions - Incorrect state before/after a test case - Toast, Snackbar, etc

Slide 114

Slide 114 text

- Network connection (VPN) - Network speed - Back-end - Framework issues - Toast, Snackbar, etc - Custom Views - Simulate User actions - Incorrect state before/after a test case - Toast, Snackbar, etc - Performance - Noti fi cations - Device memory External dependencies Framework Test case execution Device and emulator

Slide 115

Slide 115 text

? ? ?

Slide 116

Slide 116 text

#ExploreMore UI Testing @alex_zhukovich https://alexzh.com/