$30 off During Our Annual Pro Sale. View Details »
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
290
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
290
The Unyielding Spirit Of Android - Droidcon NYC '23
adammc331
1
360
DC London: Behind The Screen
adammc331
0
46
Composing With Confidence
adammc331
1
180
The Imposter's Guide To Dependency Injection - DCSF22
adammc331
2
360
Caching With Apollo Android
adammc331
0
380
Creating A Better Developer Experience By Avoiding Legacy Code
adammc331
0
560
Take Control Of Your APIs With GraphQL
adammc331
0
380
Droidcon London: Espresso Patronum
adammc331
0
360
Other Decks in Programming
See All in Programming
スタートアップを支える技術戦略と組織づくり
pospome
8
15k
Navigation 3: 적응형 UI를 위한 앱 탐색
fornewid
1
120
C-Shared Buildで突破するAI Agent バックテストの壁
po3rin
0
180
ソフトウェア設計の課題・原則・実践技法
masuda220
PRO
24
21k
UIデザインに役立つ 2025年の最新CSS / The Latest CSS for UI Design 2025
clockmaker
17
6.6k
分散DBって何者なんだ... Spannerから学ぶRDBとの違い
iwashi623
0
170
社内オペレーション改善のためのTypeScript / TSKaigi Hokuriku 2025
dachi023
1
470
令和最新版Android Studioで化石デバイス向けアプリを作る
arkw
0
110
手が足りない!兼業データエンジニアに必要だったアーキテクチャと立ち回り
zinkosuke
0
370
Microservices rules: What good looks like
cer
PRO
0
540
ID管理機能開発の裏側 高速にSaaS連携を実現したチームのAI活用編
atzzcokek
0
190
AIコーディングエージェント(Manus)
kondai24
0
120
Featured
See All Featured
How to train your dragon (web standard)
notwaldorf
97
6.4k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
37
2.6k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
48
9.8k
Building Adaptive Systems
keathley
44
2.9k
It's Worth the Effort
3n
187
29k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
285
14k
Git: the NoSQL Database
bkeepers
PRO
432
66k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
9
1k
Site-Speed That Sticks
csswizardry
13
990
Code Review Best Practice
trishagee
73
19k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
690
We Have a Design System, Now What?
morganepeng
54
7.9k
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