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
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
Adam McNeilly
October 27, 2022
Programming
320
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
DC London: Composing With Confidence
Another version of Composing With Confidence, this time presented at Droidcon London.
Adam McNeilly
October 27, 2022
More Decks by Adam McNeilly
See All by Adam McNeilly
MVWTF 2024: Demystifying Architecture Patterns
adammc331
1
330
The Unyielding Spirit Of Android - Droidcon NYC '23
adammc331
1
410
DC London: Behind The Screen
adammc331
0
94
Composing With Confidence
adammc331
1
220
The Imposter's Guide To Dependency Injection - DCSF22
adammc331
2
400
Caching With Apollo Android
adammc331
0
420
Creating A Better Developer Experience By Avoiding Legacy Code
adammc331
0
620
Take Control Of Your APIs With GraphQL
adammc331
0
470
Droidcon London: Espresso Patronum
adammc331
0
460
Other Decks in Programming
See All in Programming
PHPだって関数型したい 〜できること、できないこと〜 / fp-in-php
jsoizo
0
190
トークンをケチるな、設計しろ:GitHub Copilotを賢く使うコンテキスト戦略
ochtum
0
310
才能?センス?知らん、 続けたもん勝ちだ。-- 結婚・出産・癌を越えてなお、私がプロダクトを創り続ける理由
16bitidol
2
840
Snowflake Summitでの新機能 CoCo / CoWork / snowflake-summit-2026-overall-what-new-coco
tatsuhiro
1
220
どこまでゆるくて許されるのか
tk3fftk
0
470
任せる範囲はこう広がった / How the Scope of AI Delegation Has Expanded
nrslib
1
250
ランチタイムLT会3周年!ランチタイムLT会を3年間続けられたお話
y0hgi
1
140
act2-costs.pdf
sumedhbala
0
110
act1-costs.pdf
sumedhbala
0
210
LLMによるContent Moderationの本番運用の裏側と品質担保への挑戦
suikabar
3
840
『コードを書く以外の』エンジニアリング〜課金基盤移行プロジェクト推進のためのTips4選
yuriko1211
0
330
技術記事、 専門家としてのプログラマ、 言語化
mizchi
14
7.4k
Featured
See All Featured
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
Code Review Best Practice
trishagee
74
20k
Darren the Foodie - Storyboard
khoart
PRO
3
3.4k
Primal Persuasion: How to Engage the Brain for Learning That Lasts
tmiket
0
390
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
0
340
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
12
1.7k
Practical Orchestrator
shlominoach
191
11k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
170
We Are The Robots
honzajavorek
0
270
Typedesign – Prime Four
hannesfritz
42
3.1k
Abbi's Birthday
coloredviolet
3
8.6k
The agentic SEO stack - context over prompts
schlessera
0
840
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