Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
DC London: Composing With Confidence
Search
Adam McNeilly
October 27, 2022
Programming
0
280
DC London: Composing With Confidence
Another version of Composing With Confidence, this time presented at Droidcon London.
Adam McNeilly
October 27, 2022
Tweet
Share
More Decks by Adam McNeilly
See All by Adam McNeilly
MVWTF 2024: Demystifying Architecture Patterns
adammc331
1
280
The Unyielding Spirit Of Android - Droidcon NYC '23
adammc331
1
350
DC London: Behind The Screen
adammc331
0
36
Composing With Confidence
adammc331
1
170
The Imposter's Guide To Dependency Injection - DCSF22
adammc331
2
350
Caching With Apollo Android
adammc331
0
370
Creating A Better Developer Experience By Avoiding Legacy Code
adammc331
0
540
Take Control Of Your APIs With GraphQL
adammc331
0
360
Droidcon London: Espresso Patronum
adammc331
0
330
Other Decks in Programming
See All in Programming
ファインディ株式会社におけるMCP活用とサービス開発
starfish719
0
2k
1から理解するWeb Push
dora1998
7
1.9k
Android端末で実現するオンデバイスLLM 2025
masayukisuda
1
170
請來的 AI Agent 同事們在寫程式時,怎麼用 pytest 去除各種幻想與盲點
keitheis
0
130
概念モデル→論理モデルで気をつけていること
sunnyone
3
300
How Android Uses Data Structures Behind The Scenes
l2hyunwoo
0
480
Namespace and Its Future
tagomoris
6
710
Azure SRE Agentで運用は楽になるのか?
kkamegawa
0
2.5k
Tool Catalog Agent for Bedrock AgentCore Gateway
licux
7
2.5k
時間軸から考えるTerraformを使う理由と留意点
fufuhu
16
4.8k
スケールする組織の実現に向けた インナーソース育成術 - ISGT2025
teamlab
PRO
2
160
複雑なドメインに挑む.pdf
yukisakai1225
5
1.2k
Featured
See All Featured
Six Lessons from altMBA
skipperchong
28
4k
Rails Girls Zürich Keynote
gr2m
95
14k
The Invisible Side of Design
smashingmag
301
51k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Producing Creativity
orderedlist
PRO
347
40k
Designing for Performance
lara
610
69k
Fashionably flexible responsive web design (full day workshop)
malarkey
407
66k
Bootstrapping a Software Product
garrettdimon
PRO
307
110k
Building Better People: How to give real-time feedback that sticks.
wjessup
368
19k
How STYLIGHT went responsive
nonsquared
100
5.8k
Java REST API Framework Comparison - PWX 2021
mraible
33
8.8k
The Cult of Friendly URLs
andyhume
79
6.6k
Transcript
Composing With Confidence Adam McNeilly - @AdamMc331 @AdamMc331 #DCLDN22 1
Testing Is Important @AdamMc331 #DCLDN22 2
With New Tools Comes New Responsibilities @AdamMc331 #DCLDN22 3
Getting Started With Compose Testing1 1 https://goo.gle/compose-testing @AdamMc331 #DCLDN22 4
Two Options For Compose Testing @AdamMc331 #DCLDN22 5
Two Options For Compose Testing • Individual components @AdamMc331 #DCLDN22
5
Two Options For Compose Testing • Individual components • Activities
@AdamMc331 #DCLDN22 5
Compose Rule Setup class PrimaryButtonTest { // When testing individual
components, we can just create a compose rule. @get:Rule val composeTestRule = createComposeRule() } @AdamMc331 #DCLDN22 6
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
Rendering Content class PrimaryButtonTest { // ... @Test fun renderEnabledButton()
{ composeTestRule.setContent { PrimaryButton( text = "Test Button", enabled = true, ) } } } @AdamMc331 #DCLDN22 8
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
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
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
Finding Components @AdamMc331 #DCLDN22 10
Finding Components composeTestRule.onNode(matcher) composeTestRule.onNode(hasProgressBarRangeInfo(...)) composeTestRule.onNode(isDialog()) @AdamMc331 #DCLDN22 11
Finding Components composeTestRule.onNode(matcher) composeTestRule.onNode(hasProgressBarRangeInfo(...)) composeTestRule.onNode(isDialog()) // Helpers composeTestRule.onNodeWithText("...") // Multiple
composeTestRule.onAllNodes(...) @AdamMc331 #DCLDN22 12
Making Assertions @AdamMc331 #DCLDN22 13
Making Assertions composeTestRule .onNode(...) .assert(matcher) composeTestRule .onNode(...) .assert(hasText("Test Button")) composeTestRule
.onNode(...) .assert(isEnabled()) @AdamMc331 #DCLDN22 14
Making Assertions composeTestRule .onNode(...) .assert(matcher) // Helpers composeTestRule .onNode(...) .assertTextEquals("Test
Button") @AdamMc331 #DCLDN22 15
Performing Actions @AdamMc331 #DCLDN22 16
Performing Actions composeTestRule .onNode(...) .performClick() composeTestRule .onNode(...) .performTextInput(...) @AdamMc331 #DCLDN22
17
Cheat Sheet2 2 https://developer.android.com/static/images/jetpack/compose/compose-testing-cheatsheet.png @AdamMc331 #DCLDN22 18
Test Tags @AdamMc331 #DCLDN22 19
Test Tags // In app PrimaryButton( modifier = Modifier.testTag("login_button") )
// In test composeTestRule.onNodeWithTag("login_button") @AdamMc331 #DCLDN22 20
Let's Test A Component @AdamMc331 #DCLDN22 21
Primary Button @Composable fun PrimaryButton( text: String, onClick: () ->
Unit, enabled: Boolean = true, ) @AdamMc331 #DCLDN22 22
Primary Button @Composable fun PrimaryButton( text: String, onClick: () ->
Unit, enabled: Boolean = true, ) @AdamMc331 #DCLDN22 22
Setup @RunWith(AndroidJUnit4::class) class PrimaryButtonTest { @get:Rule val composeTestRule = createComposeRule()
@Test fun handleClickWhenEnabled() { // ... } } @AdamMc331 #DCLDN22 23
Render Content var wasClicked = false composeTestRule.setContent { PrimaryButton( text
= "Test Button", onClick = { wasClicked = true }, enabled = true, ) } @AdamMc331 #DCLDN22 24
Verify Behavior composeTestRule .onNodeWithText("Test Button") .performClick() assertTrue(wasClicked) @AdamMc331 #DCLDN22 25
A Bigger Component @AdamMc331 #DCLDN22 26
@AdamMc331 #DCLDN22 27
@AdamMc331 #DCLDN22 28
Test Setup @Test fun renderBlueWinner() { composeTestRule.setContent { PocketLeagueTheme {
MatchCard( match = MatchDetailDisplayModel.blueWinner, ) } } } @AdamMc331 #DCLDN22 29
@AdamMc331 #DCLDN22 30
Trophy Icon? @AdamMc331 #DCLDN22 31
Let's Debug @Test fun renderBlueWinner() { composeTestRule.setContent { ... }
composeTestRule.onRoot().printToLog(tag = "BLUE_WINNER") } @AdamMc331 #DCLDN22 32
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
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
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
Another Option @AdamMc331 #DCLDN22 35
36
37
Sample class MatchCardPaparazziTest { @get:Rule val paparazzi = Paparazzi() @Test
fun renderBlueTeamWinner() { paparazzi.snapshot { PocketLeagueTheme { MatchCard(match = MatchDetailDisplayModel.blueWinner) } } } } @AdamMc331 #DCLDN22 38
@AdamMc331 #DCLDN22 39
40
The Right Tool? @AdamMc331 #DCLDN22 41
The Right Tool? • Paparazzi has pixel perfect validation @AdamMc331
#DCLDN22 41
The Right Tool? • Paparazzi has pixel perfect validation •
Requires you to verfy snapshot @AdamMc331 #DCLDN22 41
The Right Tool? • Paparazzi has pixel perfect validation •
Requires you to verfy snapshot • Snapshots can change often @AdamMc331 #DCLDN22 41
When deciding how to test a component, consider functionality vs
rendering. @AdamMc331 #DCLDN22 42
Testing A Login Form @AdamMc331 #DCLDN22 43
@AdamMc331 #DCLDN22 44
Setup @RunWith(AndroidJUnit4::class) class MainActivityTest { @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test fun successfulLogin() { // ... } } @AdamMc331 #DCLDN22 45
Verify Login Button Disabled composeTestRule .onNodeWithTag("login_button") .assertIsNotEnabled() @AdamMc331 #DCLDN22 46
Type Username composeTestRule .onNodeWithTag("username_text_field") .performTextInput("adammc331") @AdamMc331 #DCLDN22 47
Type Password composeTestRule .onNodeWithTag("password_text_field") .performTextInput("Hunter2") @AdamMc331 #DCLDN22 48
Verify Login Button Enabled composeTestRule .onNodeWithTag("login_button") .assertIsEnabled() @AdamMc331 #DCLDN22 49
Click Login Button composeTestRule .onNodeWithTag("login_button") .performClick() @AdamMc331 #DCLDN22 50
Verify Home Screen Displayed composeTestRule .onNodeWithTag("home_screen_label") .assertIsDisplayed() @AdamMc331 #DCLDN22 51
@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
Test Robots @AdamMc331 #DCLDN22 53
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
LoginScreenRobot class LoginScreenRobot { // ... fun enterUsername(username: String) {
usernameInput.performTextInput(username) } fun enterPassword(password: String) { passwordInput.performTextInput(password) } @AdamMc331 #DCLDN22 55
Kotlin Magic fun loginScreenRobot( composeTestRule: ComposeTestRule, block: LoginScreenRobot.() -> Unit,
) { val robot = LoginScreenRobot(composeTestRule) robot.invoke(block) } @AdamMc331 #DCLDN22 56
Kotlin Magic loginScreenRobot(composeTestRule) { verifyLoginButtonDisabled() enterUsername("adammc331") enterPassword("Hunter2") verifyLoginButtonEnabled() clickLoginButton() }
@AdamMc331 #DCLDN22 57
@Test fun successfulLogin() { loginScreenRobot(composeTestRule) { verifyLoginButtonDisabled() enterUsername("adammc331") enterPassword("Hunter2") verifyLoginButtonEnabled()
clickLoginButton() } homeScreenRobot(composeTestRule) { verifyLabelDisplayed() } } @AdamMc331 #DCLDN22 58
Project Links @AdamMc331 #DCLDN22 59
Project Links • https://github.com/adammc331/PocketLeague @AdamMc331 #DCLDN22 59
Project Links • https://github.com/adammc331/PocketLeague • https://github.com/adammc331/ComposingWithConfidence @AdamMc331 #DCLDN22 59