Save 37% off PRO during our Black Friday Sale! »

An introduction Effective Snapshot Testing on Android

An introduction Effective Snapshot Testing on Android

Talk on Snapshot testing at Droidcon Berlin 2021 & Droidcon London 2021

5d6acde1f252bfe1480b057186879025?s=128

Sergio Sastre Flórez

October 30, 2021
Tweet

Transcript

  1. EFFECTIVE SNAPSHOT TESTING ON ANDROID BY SERGIO SASTRE AN INTRODUCTION

    TO
  2. WHAT Snapshot testing is

  3. Special type of UI tests take a screenshot of it,

    compare it to its reference we inflate a view, where and
  4. Snapshot tests Ui tests thousand handful Source: "Building Mobile Apps

    at scale - 39 Engineering Challenges"
  5. Snapshot tests Ui tests thousand handful ≈ 1 600 ≈

    500 Source: "Building Mobile Apps at scale - 39 Engineering Challenges"
  6. Snapshot tests Ui tests thousand handful ≈ 1 600 ≈

    500 ≈ 2 300 ≈ 20 Source: "Building Mobile Apps at scale - 39 Engineering Challenges"
  7. Snapshot tests Ui tests thousand handful ≈ 1 600 ≈

    500 ≈ 2 300 ≈ 20 ≈ 30 000 ? Source: "Building Mobile Apps at scale - 39 Engineering Challenges"
  8. Ui tests Snapshot tests Icons made by “Freepik" from "Flaticon"

    writing speed
  9. Ui tests Snapshot tests Icons made by “Freepik" from "Flaticon"

    writing speed
  10. Ui tests Snapshot tests 5- 10 sec Icons made by

    “Freepik" from "Flaticon" writing speed
  11. Ui tests Snapshot tests 5- 10 sec Icons made by

    “Freepik" from "Flaticon" writing speed
  12. Ui tests Snapshot tests 5- 10 sec 1 sec <

    Icons made by “Freepik" from "Flaticon" writing speed
  13. VS Snapshot 
 tests User interface tests

  14. Snapshot 
 tests User interface tests VS

  15. Snapshot 
 tests User interface tests +

  16. Snapshot 
 tests User interface tests + (interaction) WHAT

  17. Snapshot 
 tests User interface tests + (interaction) WHAT HOW

    (visuals)
  18. MOTIVATION Snapshot test to

  19. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  20. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  21. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  22. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  23. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  24. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs which will expand till the end of the line and …
  25. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  26. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  27. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  28. Unexpected changes from library updates Spacing, styling, themes… Layout correctness

    Long text View overlapping Languages (RTL) System font size (HUGE) 1 2 3 visual bugs
  29. Example

  30. Layout validation

  31. Layout validation

  32. Settings Display Font size System font size > >

  33. Example

  34. Example

  35. Font Size Largest 6,16% Small 8,06% Large 14,22% Default 71,56%

    TT Source: Android app with 80 000 monthly active users
  36. Font Size Small 8,06% Default 71,56% TT 20, 38 %

    Large + Largest Source: Android app with 80 000 monthly active users
  37. Do not trust Layout validation BUT Manual validation is hard

  38. 1 Snapshot tests Use 2 Automate hard configs Catch regression

    bugs
  39. WORKS HOW Snapshot testing

  40. record AND verify

  41. write test CI

  42. record CI

  43. = CI

  44. CI new <code> PR +

  45. approve CI

  46. merge CI

  47. CI new <code> PR verify

  48. CI new <code> PR

  49. CI new <code> PR

  50. CI new <code> PR

  51. CI new <code> PR verify

  52. CI new <code> PR

  53. CI new <code> PR

  54. CI new <code> PR

  55. CI Intentional change forgot to record regression bug YES NO

  56. CI Emulator config matters D1 DN . . .

  57. CI Emulator config matters = D1 DN = . .

    . = OR
  58. CI Emulator config matters D1 = . . . DN

    = = OR
  59. CI Emulator config matters D1 DN . . . =

    =
  60. CI Emulator config matters = D1 = = . .

    . DN OR
  61. CI Emulator config matters = D1 = = . .

    . DN OR
  62. 1 2 More complex than standard testing Same emulator config

    everywhere process Snapshot testing
  63. How to Snapshot test EFFECTIVELY

  64. Example

  65. Example

  66. Dealing with asynchronicity Choosing test-worthy screen states More views, more

    prone to flakiness 1 2 3 snapshot testing full screen
  67. Dealing with asynchronicity Choosing test-worthy screen states More views, more

    prone to flakiness 1 2 3 snapshot testing full screen
  68. Dealing with asynchronicity Choosing test-worthy screen states More views, more

    prone to flakiness 1 2 3 snapshot testing full screen
  69. Dealing with asynchronicity Choosing test-worthy screen states More views, more

    prone to flakiness 1 2 3 snapshot testing full screen
  70. Dealing with asynchronicity Choosing test-worthy screen states More views, more

    prone to flakiness 1 2 3 snapshot testing full screen
  71. start small & simple Header Training View Text view to

    memorise 1 split into smaller testable views
  72. Header Training View Text view to memorise start small &

    simple 1 what to test
  73. Header Training View Text view to memorise start small &

    simple 1 what to test
  74. A B start small & simple 1 Identifying view states

  75. A B start small & simple 1 Identifying view states

  76. aim to break the layout 2 Identifying unhappy paths

  77. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train
  78. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages
  79. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size
  80. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size narrow screen
  81. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size narrow screen
  82. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size narrow screen trainingInfo =
 TrainingItem ( trainingByLang = wordsToTrainPerLang(999_999) , activeLangs = Language.values().toSet( ) ), TrainingTestItem( fontSize = FontScale.HUGE, viewWidth = ViewWidth.NARROW fun withWordsToTrainUnhappyPath() = )
 ) Mock Data
  83. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size narrow screen trainingInfo =
 TrainingItem ( trainingByLang = wordsToTrainPerLang(999_999) , activeLangs = Language.values().toSet( ) ), TrainingTestItem( fontSize = FontScale.HUGE, viewWidth = ViewWidth.NARROW fun withWordsToTrainUnhappyPath() = )
 )
  84. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size training 
 item narrow screen trainingInfo =
 TrainingItem ( trainingByLang = wordsToTrainPerLang(999_999) , activeLangs = Language.values().toSet( ) ), TrainingTestItem( fontSize = FontScale.HUGE, viewWidth = ViewWidth.NARROW ) fun withWordsToTrainUnhappyPath() = )
 )
  85. aim to break the layout 2 Identifying unhappy paths thousand

    of words to train words to train for all 7 languages huge system font size narrow screen trainingInfo =
 TrainingItem ( trainingByLang = wordsToTrainPerLang(999_999) , activeLangs = Language.values().toSet( ) ), TrainingTestItem( fontSize = FontScale.HUGE, viewWidth = ViewWidth.NARROW )
 ) external fun withWordsToTrainUnhappyPath() =
  86. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot private fun ScreenshotTest.snapViewHolder(testItem: TrainingTestItem, snapshotName: String){ val activity = FontsizeActivityScenario.launchWith(testItem.fontScale ) .waitForActivity(), val view = waitForView {
 val layout = activity.inflate(R.layout.training_row)
 TrainingViewHolder(layout).apply { bind ( trainingItem = testItem.trainingItem,
 languageClickedListener = null
 ) } } compareScreenshot {
 holder = view,
 widthInPx = testItem.viewWidth.widthInPx , name = snapshotName
 } } Test itself
  87. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot private fun ScreenshotTest.snapViewHolder(testItem: TrainingTestItem, snapshotName: String){ val activity = FontsizeActivityScenario.launchWith(testItem.fontScale ) .waitForActivity(), val view = waitForView {
 val layout = activity.inflate(R.layout.training_row)
 TrainingViewHolder(layout).apply { bind ( trainingItem = testItem.trainingItem,
 languageClickedListener = null
 ) } } compareScreenshot {
 holder = view,
 widthInPx = testItem.viewWidth.widthInPx , name = snapshotName
 } }
  88. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot private fun ScreenshotTest.snapViewHolder(testItem: TrainingTestItem, snapshotName: String){ val activity = FontsizeActivityScenario.launchWith(testItem.fontScale ) .waitForActivity(), val view = waitForView {
 val layout = activity.inflate(R.layout.training_row)
 TrainingViewHolder(layout).apply { bind ( trainingItem = testItem.trainingItem,
 languageClickedListener = null
 ) } } compareScreenshot {
 holder = view,
 widthInPx = testItem.viewWidth.widthInPx , name = snapshotName
 } } Wait for 
 Ui thread 
 idle + launch 
 ActivityScenario
  89. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot private fun ScreenshotTest.snapViewHolder(testItem: TrainingTestItem, snapshotName: String){ val activity = FontsizeActivityScenario.launchWith(testItem.fontScale ) .waitForActivity(), val view = waitForView {
 val layout = activity.inflate(R.layout.training_row)
 TrainingViewHolder(layout).apply { bind ( trainingItem = testItem.trainingItem,
 languageClickedListener = null
 ) } } compareScreenshot {
 holder = view,
 widthInPx = testItem.viewWidth.widthInPx , name = snapshotName
 } } Inflate view 
 & bind data Wait for 
 Ui thread 
 idle +
  90. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot private fun ScreenshotTest.snapViewHolder(testItem: TrainingTestItem, snapshotName: String){ val activity = FontsizeActivityScenario.launchWith(testItem.fontScale ) .waitForActivity(), val view = waitForView {
 val layout = activity.inflate(R.layout.training_row)
 TrainingViewHolder(layout).apply { bind ( trainingItem = testItem.trainingItem,
 languageClickedListener = null
 ) } } compareScreenshot {
 holder = view,
 widthInPx = testItem.viewWidth.widthInPx , name = snapshotName
 } } record screenshot
  91. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot private fun ScreenshotTest.snapViewHolder(testItem: TrainingTestItem, snapshotName: String){ val activity = FontsizeActivityScenario.launchWith(testItem.fontScale ) .waitForActivity(), val view = waitForView {
 val layout = activity.inflate(R.layout.training_row)
 TrainingViewHolder(layout).apply { bind ( trainingItem = testItem.trainingItem,
 languageClickedListener = null
 ) } } compareScreenshot {
 holder = view,
 widthInPx = testItem.viewWidth.widthInPx , name = snapshotName
 } }
  92. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } Parameterized 
 tests
  93. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) }
  94. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } set 
 Runner
  95. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } generate 
 test data
  96. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } test with 
 parameter
  97. aim to break the layout 2 writing the snapshot test

    with pedrovgs/Shot @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) }
  98. aim to break the layout 2 thousand of words to

    train words to train for all 7 languages huge system font size narrow screen recording unhappy path
  99. aim to break the layout 2 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val

    testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath()
 .. . )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } recording happy and unhappy paths
  100. aim to break the layout 2 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(private val

    testItem: TrainingTestItem): ScreenshotTest{ companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(), withWordsToTrainHappyPath()
 withoutWordsToTrainUnhappyPath(), withoutWordsToTrainHappyPath( ) )
 } @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } recording happy and unhappy paths
  101. aim to break the layout 2 recording happy and unhappy

    paths without WordsToTrain HappyPath() with WordsToTrain HappyPath() with WordsToTrain UnhappyPath() without WordsToTrain UnhappyPath()
  102. group tests by relevance 3

  103. 2’ 100 happy + unhappy group tests by relevance 3

    100%
  104. 2’ 4’ 200 100 happy + unhappy group tests by

    relevance 3 100%
  105. 2’ 4’ 8’ 400 200 100 happy + unhappy group

    tests by relevance 3 100%
  106. 2’ 4’ 8’ 16’ 800 400 200 100 happy +

    unhappy group tests by relevance 3 100%
  107. 2’ 4’ 8’ 16’ … 800 400 200 100 happy

    + unhappy 100% group tests by relevance 3
  108. 2’ 4’ 8’ 16’ 800 400 200 100 group tests

    by relevance 3 unhappy happy 90 - 100% 5 - 20% …
  109. 2’ … 1’ 3’ 40 80 160 320 6’ unhappy

    happy 90 - 100% 5 - 20% group tests by relevance 3
  110. daily builds night build

  111. night build run happy path tests ONLY catch most important

    bugs run on every PR daily builds
  112. night build run happy path tests ONLY catch most important

    bugs run on every PR run unhappy path tests ONLY catch less important bugs run once a day daily builds
  113. night build run happy path tests ONLY catch most important

    bugs run on every PR run unhappy path tests ONLY catch less important bugs run once a day daily builds fast development cycles
  114. 3 unhappy vs. happy path @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val

    testItem: TrainingTestItem
 ): ScreenshotTest{ @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(), withWordsToTrainHappyPath()
 withoutWordsToTrainUnhappyPath(), withoutWordsToTrainHappyPath( ) )
 } Filtered parameterized 
 tests
  115. 3 unhappy vs. happy path @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val

    testItem: TrainingTestItem
 ): ScreenshotTest{ @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(), withWordsToTrainHappyPath()
 withoutWordsToTrainUnhappyPath(), withoutWordsToTrainHappyPath( ) )
 }
  116. 3 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{

    @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(), withWordsToTrainHappyPath()
 withoutWordsToTrainUnhappyPath(), withoutWordsToTrainHappyPath( ) )
 } unhappy vs. happy path
  117. 3 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{

    @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(),
 withoutWordsToTrainUnhappyPath( ) )
 } companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainHappyPath()
 withoutWordsToTrainHappyPath( ) )
 } @RunWith(Parameterized::class)
 class TrainingItemHappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{ @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } unhappy vs. happy path
  118. 3 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{

    @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(),
 withoutWordsToTrainUnhappyPath( ) )
 } companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainHappyPath()
 withoutWordsToTrainHappyPath( ) )
 } @RunWith(Parameterized::class)
 class TrainingItemHappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{ @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } unhappy vs. happy path
  119. 3 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{

    @UnhappyPat h @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(),
 withoutWordsToTrainUnhappyPath( ) )
 } companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainHappyPath()
 withoutWordsToTrainHappyPath( ) )
 } @RunWith(Parameterized::class)
 class TrainingItemHappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{ @HappyPat h @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } unhappy vs. happy path
  120. 3 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{

    @UnhappyPat h @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(),
 withoutWordsToTrainUnhappyPath( ) )
 } companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainHappyPath()
 withoutWordsToTrainHappyPath( ) )
 } @RunWith(Parameterized::class)
 class TrainingItemHappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{ @HappyPat h @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } = path.to.annotation.UnhappyPath -Pandroid.testIntrumentationRunnerArguments.annotation ./gradlew -Precord unhappy vs. happy path
  121. 3 @RunWith(Parameterized::class)
 class TrainingItemUnhappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{

    @UnhappyPat h @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } group tests by relevance companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainUnhappyPath(),
 withoutWordsToTrainUnhappyPath( ) )
 } companion object {
 @JvmStatic
 @Parameterized.Parameter s fun testData(): Array<TrainingTestItem> = arrayOf ( withWordsToTrainHappyPath()
 withoutWordsToTrainHappyPath( ) )
 } @RunWith(Parameterized::class)
 class TrainingItemHappyPath(
 private val testItem: TrainingTestItem
 ): ScreenshotTest{ @HappyPat h @Test
 fun snapTrainingItem(){ snapViewHolder(testItem)
 ) } = path.to.annotation.HappyPath -Pandroid.testIntrumentationRunnerArguments.annotation ./gradlew -Precord unhappy vs. happy path
  122. 1 Snapshot testing effectively 2 Start small & simple Aim

    to break the layout 3 Group tests by relevance
  123. attention for your THANKS

  124. code blogs @SergioSastre @sergio-sastre @sergio-sastre @Gio_Sastre Sergio Sastre Flórez Lead

    & Senior Android developer appdev.de