Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DC London: Composing With Confidence

DC London: Composing With Confidence

Another version of Composing With Confidence, this time presented at Droidcon London.

Adam McNeilly

October 27, 2022
Tweet

More Decks by Adam McNeilly

Other Decks in Programming

Transcript

  1. Composing With Confidence
    Adam McNeilly - @AdamMc331
    @AdamMc331
    #DCLDN22 1

    View Slide

  2. Testing Is Important
    @AdamMc331
    #DCLDN22 2

    View Slide

  3. With New Tools Comes New
    Responsibilities
    @AdamMc331
    #DCLDN22 3

    View Slide

  4. Getting Started With Compose
    Testing1
    1 https://goo.gle/compose-testing
    @AdamMc331
    #DCLDN22 4

    View Slide

  5. Two Options For Compose Testing
    @AdamMc331
    #DCLDN22 5

    View Slide

  6. Two Options For Compose Testing
    • Individual components
    @AdamMc331
    #DCLDN22 5

    View Slide

  7. Two Options For Compose Testing
    • Individual components
    • Activities
    @AdamMc331
    #DCLDN22 5

    View Slide

  8. Compose Rule Setup
    class PrimaryButtonTest {
    // When testing individual components, we can just create a compose rule.
    @get:Rule
    val composeTestRule = createComposeRule()
    }
    @AdamMc331
    #DCLDN22 6

    View Slide

  9. Compose Rule Setup
    class PrimaryButtonTest {
    // When testing individual components, we can just create a compose rule.
    @get:Rule
    val composeTestRule = createComposeRule()
    // When testing activities, use androidComposeRule.
    @get:Rule
    val composeTestRule = createAndroidComposeRule()
    }
    @AdamMc331
    #DCLDN22 7

    View Slide

  10. Rendering Content
    class PrimaryButtonTest {
    // ...
    @Test
    fun renderEnabledButton() {
    composeTestRule.setContent {
    PrimaryButton(
    text = "Test Button",
    enabled = true,
    )
    }
    }
    }
    @AdamMc331
    #DCLDN22 8

    View Slide

  11. Test Recipe
    // Find component
    composeTestRule.onNodeWithText("Test Button")
    // Make assertion
    composeTestRule.onNodeWithText("Test Button")
    .assertIsEnabled()
    // Perform action
    composeTestRule.onNodeWithText("Test Button")
    .performClick()
    @AdamMc331
    #DCLDN22 9

    View Slide

  12. Test Recipe
    // Find component
    composeTestRule.onNodeWithText("Test Button")
    // Make assertion
    composeTestRule.onNodeWithText("Test Button")
    .assertIsEnabled()
    // Perform action
    composeTestRule.onNodeWithText("Test Button")
    .performClick()
    @AdamMc331
    #DCLDN22 9

    View Slide

  13. Test Recipe
    // Find component
    composeTestRule.onNodeWithText("Test Button")
    // Make assertion
    composeTestRule.onNodeWithText("Test Button")
    .assertIsEnabled()
    // Perform action
    composeTestRule.onNodeWithText("Test Button")
    .performClick()
    @AdamMc331
    #DCLDN22 9

    View Slide

  14. Finding Components
    @AdamMc331
    #DCLDN22 10

    View Slide

  15. Finding Components
    composeTestRule.onNode(matcher)
    composeTestRule.onNode(hasProgressBarRangeInfo(...))
    composeTestRule.onNode(isDialog())
    @AdamMc331
    #DCLDN22 11

    View Slide

  16. Finding Components
    composeTestRule.onNode(matcher)
    composeTestRule.onNode(hasProgressBarRangeInfo(...))
    composeTestRule.onNode(isDialog())
    // Helpers
    composeTestRule.onNodeWithText("...")
    // Multiple
    composeTestRule.onAllNodes(...)
    @AdamMc331
    #DCLDN22 12

    View Slide

  17. Making Assertions
    @AdamMc331
    #DCLDN22 13

    View Slide

  18. Making Assertions
    composeTestRule
    .onNode(...)
    .assert(matcher)
    composeTestRule
    .onNode(...)
    .assert(hasText("Test Button"))
    composeTestRule
    .onNode(...)
    .assert(isEnabled())
    @AdamMc331
    #DCLDN22 14

    View Slide

  19. Making Assertions
    composeTestRule
    .onNode(...)
    .assert(matcher)
    // Helpers
    composeTestRule
    .onNode(...)
    .assertTextEquals("Test Button")
    @AdamMc331
    #DCLDN22 15

    View Slide

  20. Performing Actions
    @AdamMc331
    #DCLDN22 16

    View Slide

  21. Performing Actions
    composeTestRule
    .onNode(...)
    .performClick()
    composeTestRule
    .onNode(...)
    .performTextInput(...)
    @AdamMc331
    #DCLDN22 17

    View Slide

  22. Cheat Sheet2
    2 https://developer.android.com/static/images/jetpack/compose/compose-testing-cheatsheet.png
    @AdamMc331
    #DCLDN22 18

    View Slide

  23. Test Tags
    @AdamMc331
    #DCLDN22 19

    View Slide

  24. Test Tags
    // In app
    PrimaryButton(
    modifier = Modifier.testTag("login_button")
    )
    // In test
    composeTestRule.onNodeWithTag("login_button")
    @AdamMc331
    #DCLDN22 20

    View Slide

  25. Let's Test A Component
    @AdamMc331
    #DCLDN22 21

    View Slide

  26. Primary Button
    @Composable
    fun PrimaryButton(
    text: String,
    onClick: () -> Unit,
    enabled: Boolean = true,
    )
    @AdamMc331
    #DCLDN22 22

    View Slide

  27. Primary Button
    @Composable
    fun PrimaryButton(
    text: String,
    onClick: () -> Unit,
    enabled: Boolean = true,
    )
    @AdamMc331
    #DCLDN22 22

    View Slide

  28. Setup
    @RunWith(AndroidJUnit4::class)
    class PrimaryButtonTest {
    @get:Rule
    val composeTestRule = createComposeRule()
    @Test
    fun handleClickWhenEnabled() {
    // ...
    }
    }
    @AdamMc331
    #DCLDN22 23

    View Slide

  29. Render Content
    var wasClicked = false
    composeTestRule.setContent {
    PrimaryButton(
    text = "Test Button",
    onClick = {
    wasClicked = true
    },
    enabled = true,
    )
    }
    @AdamMc331
    #DCLDN22 24

    View Slide

  30. Verify Behavior
    composeTestRule
    .onNodeWithText("Test Button")
    .performClick()
    assertTrue(wasClicked)
    @AdamMc331
    #DCLDN22 25

    View Slide

  31. A Bigger Component
    @AdamMc331
    #DCLDN22 26

    View Slide

  32. @AdamMc331
    #DCLDN22 27

    View Slide

  33. @AdamMc331
    #DCLDN22 28

    View Slide

  34. Test Setup
    @Test
    fun renderBlueWinner() {
    composeTestRule.setContent {
    PocketLeagueTheme {
    MatchCard(
    match = MatchDetailDisplayModel.blueWinner,
    )
    }
    }
    }
    @AdamMc331
    #DCLDN22 29

    View Slide

  35. @AdamMc331
    #DCLDN22 30

    View Slide

  36. Trophy Icon?
    @AdamMc331
    #DCLDN22 31

    View Slide

  37. Let's Debug
    @Test
    fun renderBlueWinner() {
    composeTestRule.setContent { ... }
    composeTestRule.onRoot().printToLog(tag = "BLUE_WINNER")
    }
    @AdamMc331
    #DCLDN22 32

    View Slide

  38. Debug Output
    printToLog:
    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=237.0, r=1080.0, b=609.0)px
    |-Node #2 at (l=0.0, t=237.0, r=1080.0, b=609.0)px
    // ...
    |-Node #6 at (l=115.0, t=436.0, r=332.0, b=495.0)px, Tag: 'blue_match_team_name'
    | Text = '[Knights [winner]]'
    | Actions = [GetTextLayoutResult]
    // ...
    @AdamMc331
    #DCLDN22 33

    View Slide

  39. Assertions
    @Test
    fun renderBlueWinner() {
    composeTestRule.setContent { ... }
    composeTestRule
    .onNodeWithTag("blue_match_team_name")
    .assertTextEquals("Knights [winner]")
    composeTestRule
    .onNodeWithTag("orange_match_team_name")
    .assertTextEquals("G2 Esports")
    }
    @AdamMc331
    #DCLDN22 34

    View Slide

  40. Assertions
    @Test
    fun renderBlueWinner() {
    composeTestRule.setContent { ... }
    composeTestRule
    .onNodeWithTag("blue_match_team_name")
    .assertTextEquals("Knights [winner]")
    composeTestRule
    .onNodeWithTag("orange_match_team_name")
    .assertTextEquals("G2 Esports")
    }
    @AdamMc331
    #DCLDN22 34

    View Slide

  41. Another Option
    @AdamMc331
    #DCLDN22 35

    View Slide

  42. 36

    View Slide

  43. 37

    View Slide

  44. Sample
    class MatchCardPaparazziTest {
    @get:Rule
    val paparazzi = Paparazzi()
    @Test
    fun renderBlueTeamWinner() {
    paparazzi.snapshot {
    PocketLeagueTheme {
    MatchCard(match = MatchDetailDisplayModel.blueWinner)
    }
    }
    }
    }
    @AdamMc331
    #DCLDN22 38

    View Slide

  45. @AdamMc331
    #DCLDN22 39

    View Slide

  46. 40

    View Slide

  47. The Right Tool?
    @AdamMc331
    #DCLDN22 41

    View Slide

  48. The Right Tool?
    • Paparazzi has pixel perfect validation
    @AdamMc331
    #DCLDN22 41

    View Slide

  49. The Right Tool?
    • Paparazzi has pixel perfect validation
    • Requires you to verfy snapshot
    @AdamMc331
    #DCLDN22 41

    View Slide

  50. The Right Tool?
    • Paparazzi has pixel perfect validation
    • Requires you to verfy snapshot
    • Snapshots can change often
    @AdamMc331
    #DCLDN22 41

    View Slide

  51. When deciding how to test a
    component, consider functionality
    vs rendering.
    @AdamMc331
    #DCLDN22 42

    View Slide

  52. Testing A Login Form
    @AdamMc331
    #DCLDN22 43

    View Slide

  53. @AdamMc331
    #DCLDN22 44

    View Slide

  54. Setup
    @RunWith(AndroidJUnit4::class)
    class MainActivityTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule()
    @Test
    fun successfulLogin() {
    // ...
    }
    }
    @AdamMc331
    #DCLDN22 45

    View Slide

  55. Verify Login Button Disabled
    composeTestRule
    .onNodeWithTag("login_button")
    .assertIsNotEnabled()
    @AdamMc331
    #DCLDN22 46

    View Slide

  56. Type Username
    composeTestRule
    .onNodeWithTag("username_text_field")
    .performTextInput("adammc331")
    @AdamMc331
    #DCLDN22 47

    View Slide

  57. Type Password
    composeTestRule
    .onNodeWithTag("password_text_field")
    .performTextInput("Hunter2")
    @AdamMc331
    #DCLDN22 48

    View Slide

  58. Verify Login Button Enabled
    composeTestRule
    .onNodeWithTag("login_button")
    .assertIsEnabled()
    @AdamMc331
    #DCLDN22 49

    View Slide

  59. Click Login Button
    composeTestRule
    .onNodeWithTag("login_button")
    .performClick()
    @AdamMc331
    #DCLDN22 50

    View Slide

  60. Verify Home Screen Displayed
    composeTestRule
    .onNodeWithTag("home_screen_label")
    .assertIsDisplayed()
    @AdamMc331
    #DCLDN22 51

    View Slide

  61. @Test
    fun successfulLogin() {
    composeTestRule
    .onNodeWithTag("login_button")
    .assertIsNotEnabled()
    composeTestRule
    .onNodeWithTag("username_text_field")
    .performTextInput("adammc331")
    composeTestRule
    .onNodeWithTag("login_button")
    .assertIsNotEnabled()
    composeTestRule
    .onNodeWithTag("password_text_field")
    .performTextInput("Hunter2")
    composeTestRule
    .onNodeWithTag("login_button")
    .assertIsEnabled()
    composeTestRule
    .onNodeWithTag("login_button")
    .performClick()
    composeTestRule
    .onNodeWithTag("home_screen_label")
    .assertIsDisplayed()
    }
    @AdamMc331
    #DCLDN22 52

    View Slide

  62. Test Robots
    @AdamMc331
    #DCLDN22 53

    View Slide

  63. LoginScreenRobot
    class LoginScreenRobot(
    composeTestRule: ComposeTestRule,
    ) {
    private val usernameInput = composeTestRule.onNodeWithTag("username_text_field")
    private val passwordInput = composeTestRule.onNodeWithTag("password_text_field")
    private val loginButton = composeTestRule.onNodeWithTag("login_button")
    }
    @AdamMc331
    #DCLDN22 54

    View Slide

  64. LoginScreenRobot
    class LoginScreenRobot {
    // ...
    fun enterUsername(username: String) {
    usernameInput.performTextInput(username)
    }
    fun enterPassword(password: String) {
    passwordInput.performTextInput(password)
    }
    @AdamMc331
    #DCLDN22 55

    View Slide

  65. Kotlin Magic
    fun loginScreenRobot(
    composeTestRule: ComposeTestRule,
    block: LoginScreenRobot.() -> Unit,
    ) {
    val robot = LoginScreenRobot(composeTestRule)
    robot.invoke(block)
    }
    @AdamMc331
    #DCLDN22 56

    View Slide

  66. Kotlin Magic
    loginScreenRobot(composeTestRule) {
    verifyLoginButtonDisabled()
    enterUsername("adammc331")
    enterPassword("Hunter2")
    verifyLoginButtonEnabled()
    clickLoginButton()
    }
    @AdamMc331
    #DCLDN22 57

    View Slide

  67. @Test
    fun successfulLogin() {
    loginScreenRobot(composeTestRule) {
    verifyLoginButtonDisabled()
    enterUsername("adammc331")
    enterPassword("Hunter2")
    verifyLoginButtonEnabled()
    clickLoginButton()
    }
    homeScreenRobot(composeTestRule) {
    verifyLabelDisplayed()
    }
    }
    @AdamMc331
    #DCLDN22 58

    View Slide

  68. Project Links
    @AdamMc331
    #DCLDN22 59

    View Slide

  69. Project Links
    • https://github.com/adammc331/PocketLeague
    @AdamMc331
    #DCLDN22 59

    View Slide

  70. Project Links
    • https://github.com/adammc331/PocketLeague
    • https://github.com/adammc331/ComposingWithConfidence
    @AdamMc331
    #DCLDN22 59

    View Slide