Slide 1

Slide 1 text

@alex_zhukovich https://alexzh.com/ Ef fi cient 
 UI Testing

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Exploring the app

Slide 7

Slide 7 text

ACTIVITY FRAGMENT

Slide 8

Slide 8 text

ACTIVITY FRAGMENT COMPOSABLE FUNCTION

Slide 9

Slide 9 text

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

Slide 10

Slide 10 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(

Slide 11

Slide 11 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.1"

Slide 12

Slide 12 text

launchActivity launchFragmentInContainer ComposeContentTestRule Activity Fragment

Slide 13

Slide 13 text

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

Slide 14

Slide 14 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:1:1"

Slide 15

Slide 15 text

launchActivity launchFragmentInContainer ComposeContentTestRule Component Activity @Composable

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

End-To-End testing

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

END-TO-END TESTS class ProfileE2ETest { @get:Rule val composableTestRule = createAndroidComposeRule() fun hasText(@StringRes resId: Int) = hasText(composableTestRule.activity.getString(resId)) @Test fun createAccount_emptyNameErrorMessage() { composableTestRule.apply { onNode(hasText(R.string.navigation_settings_label)) .performClick() onNode(hasText(R.string.settingsScreen_profile_title)) .performClick() onNode(hasText(R.string.profileScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label)) .assertIsDisplayed() } } }

Slide 21

Slide 21 text

END-TO-END TESTS class ProfileE2ETest { @get:Rule val composableTestRule = createAndroidComposeRule() fun hasText(@StringRes resId: Int) = hasText(composableTestRule.activity.getString(resId)) @Test fun createAccount_emptyNameErrorMessage() { composableTestRule.apply { onNode(hasText(R.string.navigation_settings_label)) .performClick() onNode(hasText(R.string.settingsScreen_profile_title)) .performClick() onNode(hasText(R.string.profileScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label)) .assertIsDisplayed() } } }

Slide 22

Slide 22 text

END-TO-END TESTS class ProfileE2ETest { @get:Rule val composableTestRule = createAndroidComposeRule() fun hasText(@StringRes resId: Int) = hasText(composableTestRule.activity.getString(resId)) @Test fun createAccount_emptyNameErrorMessage() { composableTestRule.apply { onNode(hasText(R.string.navigation_settings_label)) .performClick() onNode(hasText(R.string.settingsScreen_profile_title)) .performClick() onNode(hasText(R.string.profileScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label)) .assertIsDisplayed() } } } NAVIGATION ≈ 3 SEC

Slide 23

Slide 23 text

END-TO-END TESTS NAVIGATION Navigate to the required screen INTERACTION WITH THE SERVER Interaction with the production server ENTRY POINT Similar to the entry point of an app APP VERIFICATION Slow verification of the app

Slide 24

Slide 24 text

Functional testing

Slide 25

Slide 25 text

FUNCTIONAL TESTS class LoginScreenTest : KoinTest { @get:Rule val composableTestRule = createAndroidComposeRule() @Before fun setup() { stopKoin() startKoin { androidContext(InstrumentationRegistry.getInstrumentation().targetContext) modules(dataModule, appModule) } } @Test fun createAccount_emptyNameErrorMessage() { composableTestRule.apply { setContent { CreateAccountScreen( viewModel = get(), onLogin = { }, onCreateAccountSuccess = { } 
 ) } onNode(hasText(R.string.createAccountScreen_createAccount_button)) .performClick() onNode(hasText(R.string.createAccountScreen_error_nameIsTooShort_label)) .assertIsDisplayed() } } 
 
 fun hasText(@StringRes resId: Int) = hasText(composableTestRule.activity.getString(resId)) }

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

FUNCTIONAL TESTS NAVIGATION Usually no navigation INTERACTION WITH THE SERVER Usually interaction with non- production server ENTRY POINT Start the required screen of the app APP VERIFICATION Fast verification of components or screens

Slide 28

Slide 28 text

Screenshot testing

Slide 29

Slide 29 text

INTERACTION PIXEL PERFECTNESS

Slide 30

Slide 30 text

INTERACTION PIXEL PERFECTNESS

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

class WeekCalendarTest : ScreenshotTest { private val testDate = LocalDate.of(2022, 5, 5) @get:Rule val composeTestRule = createComposeRule() @Test fun weekCalendar_todayIsSelectedDate() { composeTestRule.setContent { AppTheme(darkTheme = theme == Theme.DARK) { WeekCalendar( startDate = testDate.minusDays(6), selectedDate = testDate.minusDays(1), onSelectedDateChange = {}, todayDate = testDate.minusDays(1) ) } } compareScreenshot(composeTestRule) } }

Slide 34

Slide 34 text

Ef fi cient UI testing

Slide 35

Slide 35 text

END-TO-END TESTS FUNCTIONAL TESTS SCREENSHOT TESTS

Slide 36

Slide 36 text

FUNCTIONAL TESTS SCREENSHOT TESTS

Slide 37

Slide 37 text

END-TO-END TESTS SCREENSHOT TESTS

Slide 38

Slide 38 text

END-TO-END TESTS FUNCTIONAL TESTS

Slide 39

Slide 39 text

END-TO-END TESTS FUNCTIONAL TESTS SCREENSHOT TESTS

Slide 40

Slide 40 text

Lesson learned

Slide 41

Slide 41 text

READABILITY

Slide 42

Slide 42 text

onNodeWithText("Login") .performClick() onNodeWithText("Email") .performTextInput(email) onNodeWithText("Password") .performTextInput(password) onNode(hasText("LOGIN")) .performClick() onNodeWithText(email) .assertIsDisplayed() onNodeWithText(username) .assertIsDisplayed()

Slide 43

Slide 43 text

// Profile screen onNodeWithText("Login") .performClick() // Login screen onNodeWithText("Email") .performTextInput(email) onNodeWithText("Password") .performTextInput(password) onNode(hasText("LOGIN")) .performClick() // Profile screen onNodeWithText(email) .assertIsDisplayed() onNodeWithText(username) .assertIsDisplayed()

Slide 44

Slide 44 text

profileScreen { launch() hasTitle() tapOnLogin() } loginScreen { login(email, password) } profileScreen { hasUserInfo(email, username) }

Slide 45

Slide 45 text

FLAKY TESTS READABILITY

Slide 46

Slide 46 text

External dependencies Framework Test case execution Device and emulator

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 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 52

Slide 52 text

LEARN YOUR TOOLS FLAKY TESTS READABILITY

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

LEARN YOUR TOOLS PARAMETERIZED TESTS FLAKY TESTS READABILITY

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

@RunWith(TestParameterInjector::class) class SettingsScreenParamScreenshotTest : ScreenshotTest { @get:Rule val composeTestRule = createEmptyComposeRule() @Test fun settingsScreen_customFontSizeAndUiMode( @TestParameter fontSize: FontSize, @TestParameter uiMode: UiMode ) { val activityScenario = ActivityScenarioConfigurator.ForComposable() .setFontSize(fontSize) .setUiMode(uiMode) .launchConfiguredActivity() .onActivity { it.setContent { AppTheme { SettingsScreen( onProfile = {}, onDocs = {} ) } } } activityScenario.waitForActivity() compareScreenshot(composeTestRule, "settingsScreen_${uiMode}_${fontSize}_defaultState") activityScenario.close() } }

Slide 60

Slide 60 text

@RunWith(TestParameterInjector::class) class SettingsScreenParamScreenshotTest : ScreenshotTest { @get:Rule val composeTestRule = createEmptyComposeRule() @Test fun settingsScreen_customFontSizeAndUiMode( @TestParameter fontSize: FontSize, @TestParameter uiMode: UiMode ) { val activityScenario = ActivityScenarioConfigurator.ForComposable() .setFontSize(fontSize) .setUiMode(uiMode) .launchConfiguredActivity() .onActivity { it.setContent { AppTheme { SettingsScreen( onProfile = {}, onDocs = {} ) } } } activityScenario.waitForActivity() compareScreenshot(composeTestRule, "settingsScreen_${uiMode}_${fontSize}_defaultState") activityScenario.close() } }

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

DIFFERENT APPROACHES LEARN YOUR TOOLS PARAMETERIZED TESTS FLAKY TESTS READABILITY

Slide 63

Slide 63 text

Server Database PRE-POPULATING THE DATABASE

Slide 64

Slide 64 text

Database DATA LAYER Query Dependencies UI interaction PRE-POPULATING THE DATABASE TEST CASE

Slide 65

Slide 65 text

Prod server Dev server Mock/Fake REAL DATA VS FAKE DATA

Slide 66

Slide 66 text

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. REAL DATA VS FAKE DATA Prod server Dev server

Slide 67

Slide 67 text

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

Slide 68

Slide 68 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 69

Slide 69 text

Next steps

Slide 70

Slide 70 text

GROUP TEST CASES TESTING WIDGETS TESTING SHORTCUTS

Slide 71

Slide 71 text

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