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

  2. Testing Is Important @AdamMc331 #DCLDN22 2

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

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

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

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

    5
  7. Two Options For Compose Testing • Individual components • Activities

    @AdamMc331 #DCLDN22 5
  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
  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<MainActivity>() } @AdamMc331 #DCLDN22 7
  10. Rendering Content class PrimaryButtonTest { // ... @Test fun renderEnabledButton()

    { composeTestRule.setContent { PrimaryButton( text = "Test Button", enabled = true, ) } } } @AdamMc331 #DCLDN22 8
  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
  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
  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
  14. Finding Components @AdamMc331 #DCLDN22 10

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

  16. Finding Components composeTestRule.onNode(matcher) composeTestRule.onNode(hasProgressBarRangeInfo(...)) composeTestRule.onNode(isDialog()) // Helpers composeTestRule.onNodeWithText("...") // Multiple

    composeTestRule.onAllNodes(...) @AdamMc331 #DCLDN22 12
  17. Making Assertions @AdamMc331 #DCLDN22 13

  18. Making Assertions composeTestRule .onNode(...) .assert(matcher) composeTestRule .onNode(...) .assert(hasText("Test Button")) composeTestRule

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

    Button") @AdamMc331 #DCLDN22 15
  20. Performing Actions @AdamMc331 #DCLDN22 16

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

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

  23. Test Tags @AdamMc331 #DCLDN22 19

  24. Test Tags // In app PrimaryButton( modifier = Modifier.testTag("login_button") )

    // In test composeTestRule.onNodeWithTag("login_button") @AdamMc331 #DCLDN22 20
  25. Let's Test A Component @AdamMc331 #DCLDN22 21

  26. Primary Button @Composable fun PrimaryButton( text: String, onClick: () ->

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

    Unit, enabled: Boolean = true, ) @AdamMc331 #DCLDN22 22
  28. Setup @RunWith(AndroidJUnit4::class) class PrimaryButtonTest { @get:Rule val composeTestRule = createComposeRule()

    @Test fun handleClickWhenEnabled() { // ... } } @AdamMc331 #DCLDN22 23
  29. Render Content var wasClicked = false composeTestRule.setContent { PrimaryButton( text

    = "Test Button", onClick = { wasClicked = true }, enabled = true, ) } @AdamMc331 #DCLDN22 24
  30. Verify Behavior composeTestRule .onNodeWithText("Test Button") .performClick() assertTrue(wasClicked) @AdamMc331 #DCLDN22 25

  31. A Bigger Component @AdamMc331 #DCLDN22 26

  32. @AdamMc331 #DCLDN22 27

  33. @AdamMc331 #DCLDN22 28

  34. Test Setup @Test fun renderBlueWinner() { composeTestRule.setContent { PocketLeagueTheme {

    MatchCard( match = MatchDetailDisplayModel.blueWinner, ) } } } @AdamMc331 #DCLDN22 29
  35. @AdamMc331 #DCLDN22 30

  36. Trophy Icon? @AdamMc331 #DCLDN22 31

  37. Let's Debug @Test fun renderBlueWinner() { composeTestRule.setContent { ... }

    composeTestRule.onRoot().printToLog(tag = "BLUE_WINNER") } @AdamMc331 #DCLDN22 32
  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
  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
  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
  41. Another Option @AdamMc331 #DCLDN22 35

  42. 36

  43. 37

  44. Sample class MatchCardPaparazziTest { @get:Rule val paparazzi = Paparazzi() @Test

    fun renderBlueTeamWinner() { paparazzi.snapshot { PocketLeagueTheme { MatchCard(match = MatchDetailDisplayModel.blueWinner) } } } } @AdamMc331 #DCLDN22 38
  45. @AdamMc331 #DCLDN22 39

  46. 40

  47. The Right Tool? @AdamMc331 #DCLDN22 41

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

    #DCLDN22 41
  49. The Right Tool? • Paparazzi has pixel perfect validation •

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

    Requires you to verfy snapshot • Snapshots can change often @AdamMc331 #DCLDN22 41
  51. When deciding how to test a component, consider functionality vs

    rendering. @AdamMc331 #DCLDN22 42
  52. Testing A Login Form @AdamMc331 #DCLDN22 43

  53. @AdamMc331 #DCLDN22 44

  54. Setup @RunWith(AndroidJUnit4::class) class MainActivityTest { @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test fun successfulLogin() { // ... } } @AdamMc331 #DCLDN22 45
  55. Verify Login Button Disabled composeTestRule .onNodeWithTag("login_button") .assertIsNotEnabled() @AdamMc331 #DCLDN22 46

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

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

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

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

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

  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
  62. Test Robots @AdamMc331 #DCLDN22 53

  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
  64. LoginScreenRobot class LoginScreenRobot { // ... fun enterUsername(username: String) {

    usernameInput.performTextInput(username) } fun enterPassword(password: String) { passwordInput.performTextInput(password) } @AdamMc331 #DCLDN22 55
  65. Kotlin Magic fun loginScreenRobot( composeTestRule: ComposeTestRule, block: LoginScreenRobot.() -> Unit,

    ) { val robot = LoginScreenRobot(composeTestRule) robot.invoke(block) } @AdamMc331 #DCLDN22 56
  66. Kotlin Magic loginScreenRobot(composeTestRule) { verifyLoginButtonDisabled() enterUsername("adammc331") enterPassword("Hunter2") verifyLoginButtonEnabled() clickLoginButton() }

    @AdamMc331 #DCLDN22 57
  67. @Test fun successfulLogin() { loginScreenRobot(composeTestRule) { verifyLoginButtonDisabled() enterUsername("adammc331") enterPassword("Hunter2") verifyLoginButtonEnabled()

    clickLoginButton() } homeScreenRobot(composeTestRule) { verifyLabelDisplayed() } } @AdamMc331 #DCLDN22 58
  68. Project Links @AdamMc331 #DCLDN22 59

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

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