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
300
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
310
The Unyielding Spirit Of Android - Droidcon NYC '23
adammc331
1
370
DC London: Behind The Screen
adammc331
0
63
Composing With Confidence
adammc331
1
190
The Imposter's Guide To Dependency Injection - DCSF22
adammc331
2
380
Caching With Apollo Android
adammc331
0
390
Creating A Better Developer Experience By Avoiding Legacy Code
adammc331
0
580
Take Control Of Your APIs With GraphQL
adammc331
0
440
Droidcon London: Espresso Patronum
adammc331
0
430
Other Decks in Programming
See All in Programming
Basic Architectures
denyspoltorak
0
680
Fragmented Architectures
denyspoltorak
0
160
AIフル活用時代だからこそ学んでおきたい働き方の心得
shinoyu
0
140
AI時代の認知負荷との向き合い方
optfit
0
160
MDN Web Docs に日本語翻訳でコントリビュート
ohmori_yusuke
0
650
AIと一緒にレガシーに向き合ってみた
nyafunta9858
0
240
HTTPプロトコル正しく理解していますか? 〜かわいい猫と共に学ぼう。ฅ^•ω•^ฅ ニャ〜
hekuchan
2
690
登壇資料を作る時に意識していること #登壇資料_findy
konifar
4
1.2k
カスタマーサクセス業務を変革したヘルススコアの実現と学び
_hummer0724
0
700
Oxlintはいいぞ
yug1224
5
1.3k
ノイジーネイバー問題を解決する 公平なキューイング
occhi
0
100
AIによる開発の民主化を支える コンテキスト管理のこれまでとこれから
mulyu
3
300
Featured
See All Featured
Navigating Weather and Climate Data
rabernat
0
110
A better future with KSS
kneath
240
18k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
122
21k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.1k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
52
5.8k
The Illustrated Children's Guide to Kubernetes
chrisshort
51
51k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
0
140
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
0
260
Practical Orchestrator
shlominoach
191
11k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
61k
From π to Pie charts
rasagy
0
120
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
117
110k
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