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
250
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
220
The Unyielding Spirit Of Android - Droidcon NYC '23
adammc331
1
290
DC London: Behind The Screen
adammc331
0
20
Composing With Confidence
adammc331
1
130
The Imposter's Guide To Dependency Injection - DCSF22
adammc331
2
300
Caching With Apollo Android
adammc331
0
320
Creating A Better Developer Experience By Avoiding Legacy Code
adammc331
0
480
Take Control Of Your APIs With GraphQL
adammc331
0
250
Droidcon London: Espresso Patronum
adammc331
0
210
Other Decks in Programming
See All in Programming
Introduce dRuby
ledsun
0
110
How to debug Xdebug... or any other weird bug in PHP
dunglas
1
470
実務未経験からいち早く戦力化するための新人エンジニア育成術 ~ 具体的な方法と育成する側の心得 ~
juri_matsuda
0
110
Cancel Next.js Page Navigation: Full Throttle
ypresto
1
180
CDKを活用した 大規模コンテナ移行 プロジェクトの紹介
yoyoyopg
0
310
標準ライブラリの動向とイテレータのパフォーマンス
makki_d
3
200
선언형 UI를 학습할 때 알아둬야하는 키워드들
l2hyunwoo
0
140
The Efficiency Paradox and How to Save Yourself and the World
hollycummins
0
170
(Deep|Web) Link support with expo-router
mrtry
0
170
CSC509 Lecture 02
javiergs
PRO
0
160
DevFest Android in Korea 2024 - 안드로이드의 문단속 : 앱을 지키는 암호화 이야기
mdb1217
1
160
5年分のツケを一気に払った話
soogie
3
1.3k
Featured
See All Featured
5 minutes of I Can Smell Your CMS
philhawksworth
202
19k
How STYLIGHT went responsive
nonsquared
95
5.1k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
37
1.7k
How to Think Like a Performance Engineer
csswizardry
16
1k
Code Review Best Practice
trishagee
62
16k
Side Projects
sachag
452
42k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
43
6.5k
Atom: Resistance is Futile
akmur
261
25k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
280
13k
Mobile First: as difficult as doing things right
swwweet
222
8.8k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
31
2.6k
Robots, Beer and Maslow
schacon
PRO
157
8.2k
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