Nitrogen in your (test) pyramid 2

Nitrogen in your (test) pyramid 2

Unit, integration, UI tests are part of the famous test pyramid, different tools were used in order to full fill it, but still today there are lot of issues about how to execute it in a proper manner and how to sync all environments (local, ci, etc) in order to satisfy all the testing criteria, and having a good reports.

From IO 2018 with the announcement of project Nitrogen, Google’d like to “nitrogenize” our tests creating a single entry point and solve all issues
we had in the past. With October release of Robolectric 4.0 and androidx.test 1.0.0, both testing environments are converging on a set of common test APIs. Robolectric now supports the AndroidJUnit4 test runner, ActivityTestRule, and Espresso for interacting with UI components.

Let’s see togheter how to organise different levels of our ‘test pyramid’, with a focus to the last news/releases in order ot be ready for the Nitrogen release.

82502a841e2a637061c294399d6c5ef3?s=128

Enrico Bruno Del Zotto

September 25, 2019
Tweet

Transcript

  1. Nitrogen in your (test) pyramid @lupsyn @tamasmarton13

  2. Nitrogen in your (test) pyramid @lupsyn @tamasmarton13 v. 2.0

  3. Who I am ?

  4. Who I am ? Enrico Bruno Del Zotto

  5. Android stuff in London Who I am ? Enrico Bruno

    Del Zotto
  6. Android stuff in London Who I am ? Enrico Bruno

    Del Zotto
  7. Android stuff in London Who I am ? Enrico Bruno

    Del Zotto
  8. Who I am ?

  9. Who I am ? Tamas Marton

  10. Android stuff in Netherlands Who I am ? Tamas Marton

  11. Android stuff in Netherlands Who I am ? Tamas Marton

  12. Android stuff in Netherlands Who I am ? Tamas Marton

  13. The test pyramid

  14. The test pyramid

  15. Coverage set

  16. Unit tests

  17. Unit tests Low level of the pyramid Coverage small set

    of our code (a unit) Validate our app behaviour one class a time.
  18. Integration tests

  19. Integration tests Medium level of the pyramid Validate either interactions

    between levels of the stack within a module, or interactions between related modules. Presence of Android Context
  20. End to end tests

  21. End to end tests Higher level of the pyramid Validate

    the user journey in the app and are the slowest and most expensive kind of test. You have to run normally in a device.
  22. None
  23. Failing UI test

  24. Failing UI test Failing unit test

  25. Failing UI test Failing unit test Passing unit test

  26. Failing UI test Failing unit test Passing unit test Refactor

  27. Failing UI test Failing unit test Passing unit test Passing

    UI test Refactor
  28. Failing UI test Failing unit test Passing unit test Passing

    UI test Refactor Refactor
  29. UI tests should be agnostic.

  30. Test for all these frameworks use the same generic format

    :
  31. Test for all these frameworks use the same generic format

    : “Given, when, then”
  32. None
  33. Why learn different API to do the same thing ?

  34. None
  35. None
  36. None
  37. None
  38. None
  39. None
  40. None
  41. None
  42. None
  43. None
  44. e2e Tests Integration test Unit tests

  45. AndroidX test library JUnit

  46. Runners : AndroidJUnit4ClassRunner Rules : ActivityTestRule, ServiceTestRule AndroidX test library

    JUnit
  47. @RunWith(AndroidJUnit4::class) class MainUiTest { @get:Rule val activityRule = ActivityTestRule(MainActivity::class.java) @Before

    fun setup() { activityRule.launchActivity(Intent().putExtra("fragment", MainFragment::class.java)) } @Test fun testAssertHelloText() { onView(withId(R.id.subtitle)).check(matches(withText("Hello World!"))) } ActivityRule
  48. @RunWith(AndroidJUnit4::class) class MainUiTest { @get:Rule val activityRule = ActivityTestRule(MainActivity::class.java) @Before

    fun setup() { activityRule.launchActivity(Intent().putExtra("fragment", MainFragment::class.java)) } @Test fun testAssertHelloText() { onView(withId(R.id.subtitle)).check(matches(withText("Hello World!"))) } ActivityRule
  49. @RunWith(AndroidJUnit4::class) class MainUiTest { @get:Rule val activityRule = ActivityTestRule(MainActivity::class.java) @Before

    fun setup() { activityRule.launchActivity(Intent().putExtra("fragment", MainFragment::class.java)) } @Test fun testAssertHelloText() { onView(withId(R.id.subtitle)).check(matches(withText("Hello World!"))) } ActivityRule
  50. @RunWith(AndroidJUnit4::class) class MainUiTest { @get:Rule val activityRule = ActivityTestRule(MainActivity::class.java) @Before

    fun setup() { activityRule.launchActivity(Intent().putExtra("fragment", MainFragment::class.java)) } @Test fun testAssertHelloText() { onView(withId(R.id.subtitle)).check(matches(withText("Hello World!"))) } ActivityRule
  51. AndroidX test library Core

  52. AndroidX test library Core ApplicationProvider : Provides ability to retrieve

    the current application Context in tests
  53. AndroidX test library Core ApplicationProvider : Provides ability to retrieve

    the current application Context in tests ActivityScenario
  54. AndroidX test library Core ApplicationProvider : Provides ability to retrieve

    the current application Context in tests ActivityScenario FragmentScenario
  55. @Test fun should_clear_locations_when_activity_in_STARTED_state() { //Given val activityScenario = ActivityScenario.launch(LocationActivity::class.java) //When

    activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then assertThat(activityScenario.getState()).isEqualTo(State.STARTED) activityScenario.onActivity { assertThat(it.noLocationSelected()).isEqualTo(true) } activityScenario.close() } Activity Scenario
  56. @Test fun should_clear_locations_when_activity_in_STARTED_state() { //Given val activityScenario = ActivityScenario.launch(LocationActivity::class.java) //When

    activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then assertThat(activityScenario.getState()).isEqualTo(State.STARTED) activityScenario.onActivity { assertThat(it.noLocationSelected()).isEqualTo(true) } activityScenario.close() } Activity Scenario
  57. @Test fun should_clear_locations_when_activity_in_STARTED_state() { //Given val activityScenario = ActivityScenario.launch(LocationActivity::class.java) //When

    activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then assertThat(activityScenario.getState()).isEqualTo(State.STARTED) activityScenario.onActivity { assertThat(it.noLocationSelected()).isEqualTo(true) } activityScenario.close() } Activity Scenario
  58. @Test fun should_clear_locations_when_activity_in_STARTED_state() { //Given val activityScenario = ActivityScenario.launch(LocationActivity::class.java) //When

    activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then assertThat(activityScenario.getState()).isEqualTo(State.STARTED) activityScenario.onActivity { assertThat(it.noLocationSelected()).isEqualTo(true) } activityScenario.close() } Activity Scenario
  59. @Test fun should_clear_locations_when_activity_in_STARTED_state() { //Given val activityScenario = ActivityScenario.launch(LocationActivity::class.java) //When

    activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then assertThat(activityScenario.getState()).isEqualTo(State.STARTED) activityScenario.onActivity { assertThat(it.noLocationSelected()).isEqualTo(true) } activityScenario.close() } Activity Scenario
  60. LaunchFragmentInContainer

  61. LaunchFragmentInContainer @Test fun locationFragmentIsDisplayed() { launchFragmentInContainer<LocationFragment>() onView(withId(R.id.location_fragment)) .check(matches(isDisplayed())) }

  62. LaunchFragmentInContainer @Test fun locationFragmentIsDisplayed() { launchFragmentInContainer<LocationFragment>() onView(withId(R.id.location_fragment)) .check(matches(isDisplayed())) }

  63. LaunchFragmentInContainer @Test fun locationFragmentIsDisplayed() { launchFragmentInContainer<LocationFragment>() onView(withId(R.id.location_fragment)) .check(matches(isDisplayed())) } val

    factory = LocationFragmentFactory() val args = Bundle().apply { putString(FRAGMENT_ARG, FRAGMENT_ARG_KEY) } launchFragmentInContainer<LocationFragment>(args, factory)
  64. LaunchFragmentInContainer @Test fun locationFragmentIsDisplayed() { launchFragmentInContainer<LocationFragment>() onView(withId(R.id.location_fragment)) .check(matches(isDisplayed())) } val

    factory = LocationFragmentFactory() val args = Bundle().apply { putString(FRAGMENT_ARG, FRAGMENT_ARG_KEY) } launchFragmentInContainer<LocationFragment>(args, factory)
  65. LaunchFragmentInContainer val scenario = launchFragmentInContainer<LocationFragment>() scenario.onFragment { it.checkLocation() } [...]

    scenario.recreate() scenario.moveToState(Lifecycle.State.RESUMED)
  66. LaunchFragmentInContainer val scenario = launchFragmentInContainer<LocationFragment>() scenario.onFragment { it.checkLocation() } [...]

    scenario.recreate() scenario.moveToState(Lifecycle.State.RESUMED)
  67. LaunchFragmentInContainer val scenario = launchFragmentInContainer<LocationFragment>() scenario.onFragment { it.checkLocation() } [...]

    scenario.recreate() scenario.moveToState(Lifecycle.State.RESUMED)
  68. LaunchFragmentInContainer val scenario = launchFragmentInContainer<LocationFragment>() scenario.onFragment { it.checkLocation() } [...]

    scenario.recreate() scenario.moveToState(Lifecycle.State.RESUMED)
  69. AndroidX test library Espresso

  70. None
  71. Hierarchy

  72. hasSibling(Matcher) onView(allOf(withText("number: 123"), hasSibling(withText("contact name")))) .perform(click()) Hierarchy

  73. hasSibling(Matcher) onView(allOf(withText("number: 123"), hasSibling(withText("contact name")))) .perform(click()) hasDescendant(Matcher) onView(withId(R.id.container)) .check(matches(atPosition(0,hasDescendant(withText("Available"))))); Hierarchy

  74. hasSibling(Matcher) onView(allOf(withText("number: 123"), hasSibling(withText("contact name")))) .perform(click()) hasDescendant(Matcher) onView(withId(R.id.container)) .check(matches(atPosition(0,hasDescendant(withText("Available"))))); isRoot()

    onView(isRoot()).check(noOverlaps()); Hierarchy
  75. @Test fun should_start_selectLocationActivity_when_location_is_not_selected() { //Given Intents.init() val activityScenario = ActivityScenario.launch(LocationActivity::class.java)

    //When activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then intended(hasComponent(SelectLocationActivity::class.java.name)) Intents.release() activityScenario.close() } espresso.intent
  76. @Test fun should_start_selectLocationActivity_when_location_is_not_selected() { //Given Intents.init() val activityScenario = ActivityScenario.launch(LocationActivity::class.java)

    //When activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then intended(hasComponent(SelectLocationActivity::class.java.name)) Intents.release() activityScenario.close() } espresso.intent
  77. @Test fun should_start_selectLocationActivity_when_location_is_not_selected() { //Given Intents.init() val activityScenario = ActivityScenario.launch(LocationActivity::class.java)

    //When activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then intended(hasComponent(SelectLocationActivity::class.java.name)) Intents.release() activityScenario.close() } espresso.intent
  78. @Test fun should_start_selectLocationActivity_when_location_is_not_selected() { //Given Intents.init() val activityScenario = ActivityScenario.launch(LocationActivity::class.java)

    //When activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then intended(hasComponent(SelectLocationActivity::class.java.name)) Intents.release() activityScenario.close() } espresso.intent
  79. @Test fun should_start_selectLocationActivity_when_location_is_not_selected() { //Given Intents.init() val activityScenario = ActivityScenario.launch(LocationActivity::class.java)

    //When activityScenario.onActivity { it.clearLocation() } activityScenario.moveToState(Lifecycle.State.STARTED) //Then intended(hasComponent(SelectLocationActivity::class.java.name)) Intents.release() activityScenario.close() } espresso.intent
  80. AndroidX test library Truth

  81. Fluent assertions, it’s not AssertJ ! AssertEquals(View.visibilty, View.Visible) -> assertThat(view).isVisible()

    AndroidX test library Truth
  82. AssertJ Puzzlers

  83. AssertJ Puzzlers assertThat(primaryColors).containsAll(RED, YELLOW, BLUE);

  84. AssertJ Puzzlers assertThat(primaryColors).containsAll(RED, YELLOW, BLUE); containsAtLeast

  85. AssertJ Puzzlers assertThat(primaryColors).containsAll(RED, YELLOW, BLUE); containsAtLeast assertThat(uniqueIdGenerator.next()).isNotSameAs(uniqueIdGenerator.next());

  86. AssertJ Puzzlers assertThat(primaryColors).containsAll(RED, YELLOW, BLUE); containsAtLeast assertThat(uniqueIdGenerator.next()).isNotSameAs(uniqueIdGenerator.next()); isNotSameInstanceAs

  87. AssertJ Puzzlers assertThat(primaryColors).containsAll(RED, YELLOW, BLUE); containsAtLeast assertThat(uniqueIdGenerator.next()).isNotSameAs(uniqueIdGenerator.next()); isNotSameInstanceAs assertThat(defaults).has(new Condition<>(x

    -> x instanceof String, "a string"));
  88. AssertJ Puzzlers assertThat(primaryColors).containsAll(RED, YELLOW, BLUE); containsAtLeast assertThat(uniqueIdGenerator.next()).isNotSameAs(uniqueIdGenerator.next()); isNotSameInstanceAs assertThat(defaults).has(new Condition<>(x

    -> x instanceof String, "a string")); not supporting condition Style-Assertions
  89. Readable AssertJ Truth

  90. Readable assertFalse(middleName.isPresent()) AssertJ Truth

  91. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) AssertJ Truth

  92. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) assertTrue(googleColors.contains(PINK)); AssertJ Truth

  93. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) assertTrue(googleColors.contains(PINK)); AssertionFailedError AssertJ Truth

  94. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) assertThat(googleColors).contains(PINK); assertTrue(googleColors.contains(PINK)); AssertionFailedError AssertJ Truth

  95. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) assertThat(googleColors).contains(PINK); <[BLUE, RED, YELLOW, BLUE, GREEN, RED]>

    should have contained <PINK> assertTrue(googleColors.contains(PINK)); AssertionFailedError AssertJ Truth
  96. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) assertFalse(googleColors.contains(PINK)); assertFalse(googleColors.contains(BLACK)); ... assertThat(googleColors).contains(PINK); <[BLUE, RED, YELLOW,

    BLUE, GREEN, RED]> should have contained <PINK> assertTrue(googleColors.contains(PINK)); AssertionFailedError AssertJ Truth
  97. Readable assertThat(middleName).isAbsent() assertFalse(middleName.isPresent()) assertFalse(googleColors.contains(PINK)); assertFalse(googleColors.contains(BLACK)); ... assertThat(googleColors) .containsNoneOf(PINK, BLACK, WHITE,

    ORANGE); assertThat(googleColors).contains(PINK); <[BLUE, RED, YELLOW, BLUE, GREEN, RED]> should have contained <PINK> assertTrue(googleColors.contains(PINK)); AssertionFailedError AssertJ Truth
  98. Subject class CarSubject(metadata: FailureMetadata, subject: Car) : Subject<CarSubject, Car>(metadata, subject)

    { companion object { private val FACTORY = Factory<CarSubject, Car> { metadata, actual -> CarSubject(metadata, actual) } fun car(): Factory<CarSubject, Car> { return FACTORY } } fun isElectrical(): CarSubject { if (!actual().isElectrical) { failWithActual(simpleFact("expected to be electrical")) } return this } fun hasChargingLevel(chargingLevel: Int): CarSubject { if (actual().chargingLevel != chargingLevel) { failWithActual(simpleFact("expected to be $chargingLevel")) } return this }
  99. Subject class CarSubject(metadata: FailureMetadata, subject: Car) : Subject<CarSubject, Car>(metadata, subject)

    { companion object { private val FACTORY = Factory<CarSubject, Car> { metadata, actual -> CarSubject(metadata, actual) } fun car(): Factory<CarSubject, Car> { return FACTORY } } fun isElectrical(): CarSubject { if (!actual().isElectrical) { failWithActual(simpleFact("expected to be electrical")) } return this } fun hasChargingLevel(chargingLevel: Int): CarSubject { if (actual().chargingLevel != chargingLevel) { failWithActual(simpleFact("expected to be $chargingLevel")) } return this }
  100. Subject class CarSubject(metadata: FailureMetadata, subject: Car) : Subject<CarSubject, Car>(metadata, subject)

    { companion object { private val FACTORY = Factory<CarSubject, Car> { metadata, actual -> CarSubject(metadata, actual) } fun car(): Factory<CarSubject, Car> { return FACTORY } } fun isElectrical(): CarSubject { if (!actual().isElectrical) { failWithActual(simpleFact("expected to be electrical")) } return this } fun hasChargingLevel(chargingLevel: Int): CarSubject { if (actual().chargingLevel != chargingLevel) { failWithActual(simpleFact("expected to be $chargingLevel")) } return this }
  101. Subject class CarSubject(metadata: FailureMetadata, subject: Car) : Subject<CarSubject, Car>(metadata, subject)

    { companion object { private val FACTORY = Factory<CarSubject, Car> { metadata, actual -> CarSubject(metadata, actual) } fun car(): Factory<CarSubject, Car> { return FACTORY } } fun isElectrical(): CarSubject { if (!actual().isElectrical) { failWithActual(simpleFact("expected to be electrical")) } return this } fun hasChargingLevel(chargingLevel: Int): CarSubject { if (actual().chargingLevel != chargingLevel) { failWithActual(simpleFact("expected to be $chargingLevel")) } return this }
  102. Subject

  103. Subject @Test fun `When buy a new car, then it

    should be electrical`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical() }
  104. Subject @Test fun `When buy a new car, then it

    should be electrical`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical() } @Test fun `When buy a new car, then it should be electrical, charged, and fast`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical().hasChargingLevel(100).isFast() }
  105. Subject @Test fun `When buy a new car, then it

    should be electrical`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical() } @Test fun `When buy a new car, then it should be electrical, charged, and fast`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical().hasChargingLevel(100).isFast() }
  106. Subject @Test fun `When buy a new car, then it

    should be electrical`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical() } @Test fun `When buy a new car, then it should be electrical, charged, and fast`() { val car = Car() assert_().about(CarSubject.car()).that(car).isElectrical().hasChargingLevel(100).isFast() }
  107. AndroidX test library Recap

  108. AndroidX test library 1- JUnit runners & rules 2- Core

    ApplicationProvider, Activity/Fragment scenario 3- Espresso yes you know it 4- Truth Fluent assertions, it’s not AssertJ ! Recap
  109. AndroidX test library androidx.test.runner.screenshot androidx.test.platform.ui androidx.test.filters androidx.test.uiautomator

  110. AndroidX test library Provides common test APIs across test environments

    including instrumentation tests.
  111. AndroidX test library We can now use the new Scenario

    API to launch the Activity or Fragment Use Espresso to make some view interactions Use Truth to make view assertions Run instrumented tests on JVM instead on the device using the new AndroidX test runner
  112. public final class AndroidJUnit4 extends Runner implements Filterable, Sortable {

    [...] private static String getRunnerClassName() { String runnerClassName = System.getProperty("android.junit.runner", null); if (runnerClassName == null) { // TODO: remove this logic when nitrogen is hooked up to always pass this property if (System.getProperty("java.runtime.name").toLowerCase().contains("android")) { return "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"; } else { return "org.robolectric.RobolectricTestRunner"; } } return runnerClassName; } [...] }
  113. public final class AndroidJUnit4 extends Runner implements Filterable, Sortable {

    [...] private static String getRunnerClassName() { String runnerClassName = System.getProperty("android.junit.runner", null); if (runnerClassName == null) { // TODO: remove this logic when nitrogen is hooked up to always pass this property if (System.getProperty("java.runtime.name").toLowerCase().contains("android")) { return "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"; } else { return "org.robolectric.RobolectricTestRunner"; } } return runnerClassName; } [...] }
  114. public final class AndroidJUnit4 extends Runner implements Filterable, Sortable {

    [...] private static String getRunnerClassName() { String runnerClassName = System.getProperty("android.junit.runner", null); if (runnerClassName == null) { // TODO: remove this logic when nitrogen is hooked up to always pass this property if (System.getProperty("java.runtime.name").toLowerCase().contains("android")) { return "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"; } else { return "org.robolectric.RobolectricTestRunner"; } } return runnerClassName; } [...] }
  115. Robolectric 4.x

  116. Robolectric 4.x Provides a JVM compliant version of the android.jar

    file, it handles inflation of views, resource loading, and lots of other stuff that’s implemented in native C code on Android devices. This enables you to run your Android tests in your continuous integration environment without any additional setup
  117. testImplementation Libs.junit testImplementation Libs.robolectric testImplementation Libs.Mockito.core testImplementation Libs.AndroidX.Test.archCore testImplementation Libs.AndroidX.Test.espressoCore

    testImplementation Libs.AndroidX.Test.espressoContrib testImplementation Libs.AndroidX.Test.espressoIdlingResources testImplementation Libs.AndroidX.Test.junitAssertions testImplementation Libs.AndroidX.Test.core testImplementation Libs.AndroidX.Test.runner testImplementation Libs.AndroidX.Test.rules testImplementation Libs.AndroidX.Test.espressoCore testImplementation Libs.AndroidX.Test.thruth
  118. testImplementation Libs.junit testImplementation Libs.robolectric testImplementation Libs.Mockito.core testImplementation Libs.AndroidX.Test.archCore testImplementation Libs.AndroidX.Test.espressoCore

    testImplementation Libs.AndroidX.Test.espressoContrib testImplementation Libs.AndroidX.Test.espressoIdlingResources testImplementation Libs.AndroidX.Test.junitAssertions testImplementation Libs.AndroidX.Test.core testImplementation Libs.AndroidX.Test.runner testImplementation Libs.AndroidX.Test.rules testImplementation Libs.AndroidX.Test.espressoCore testImplementation Libs.AndroidX.Test.thruth
  119. testImplementation Libs.junit testImplementation Libs.robolectric testImplementation Libs.Mockito.core testImplementation Libs.AndroidX.Test.archCore testImplementation Libs.AndroidX.Test.espressoCore

    testImplementation Libs.AndroidX.Test.espressoContrib testImplementation Libs.AndroidX.Test.espressoIdlingResources testImplementation Libs.AndroidX.Test.junitAssertions testImplementation Libs.AndroidX.Test.core testImplementation Libs.AndroidX.Test.runner testImplementation Libs.AndroidX.Test.rules testImplementation Libs.AndroidX.Test.espressoCore testImplementation Libs.AndroidX.Test.thruth
  120. testOptions { unitTests { includeAndroidResources = true } unitTests.all {

    include '**/*Tests.class' include '**/*Test.class' useJUnit { } ignoreFailures false // use afterTest to listen to the test execution results afterTest { descriptor, result -> println "Executing test for ${descriptor.name} with result: $ {result.resultType}" } systemProperty 'robolectric.logging.enabled', 'true' jvmArgs '-noverify' maxHeapSize = "2g" } }
  121. testOptions { unitTests { includeAndroidResources = true } unitTests.all {

    include '**/*Tests.class' include '**/*Test.class' useJUnit { } ignoreFailures false // use afterTest to listen to the test execution results afterTest { descriptor, result -> println "Executing test for ${descriptor.name} with result: $ {result.resultType}" } systemProperty 'robolectric.logging.enabled', 'true' jvmArgs '-noverify' maxHeapSize = "2g" } }
  122. testOptions { unitTests { includeAndroidResources = true } unitTests.all {

    include '**/*Tests.class' include '**/*Test.class' useJUnit { } ignoreFailures false // use afterTest to listen to the test execution results afterTest { descriptor, result -> println "Executing test for ${descriptor.name} with result: $ {result.resultType}" } systemProperty 'robolectric.logging.enabled', 'true' jvmArgs '-noverify' maxHeapSize = "2g" } }
  123. class RepositoryListUiTest : BaseInstrumentationTestCase() { @Test fun willDisplayFirstPaginatedListCorrectly() { given.gitHubServer.returnsReposListPageOneSuccessfully()

    `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withSuccessfullyLoadedFirstPageItem() } @Test fun willDisplayErrorCorrectly() { given.gitHubServer.returnErrorOnFetchingRepositories() `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withErrorState() } }
  124. class RepositoryListUiTest : BaseInstrumentationTestCase() { @Test fun willDisplayFirstPaginatedListCorrectly() { given.gitHubServer.returnsReposListPageOneSuccessfully()

    `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withSuccessfullyLoadedFirstPageItem() } @Test fun willDisplayErrorCorrectly() { given.gitHubServer.returnErrorOnFetchingRepositories() `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withErrorState() } }
  125. class RepositoryListUiTest : BaseInstrumentationTestCase() { @Test fun willDisplayFirstPaginatedListCorrectly() { given.gitHubServer.returnsReposListPageOneSuccessfully()

    `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withSuccessfullyLoadedFirstPageItem() } @Test fun willDisplayErrorCorrectly() { given.gitHubServer.returnErrorOnFetchingRepositories() `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withErrorState() } }
  126. class RepositoryListUiTest : BaseInstrumentationTestCase() { @Test fun willDisplayFirstPaginatedListCorrectly() { given.gitHubServer.returnsReposListPageOneSuccessfully()

    `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withSuccessfullyLoadedFirstPageItem() } @Test fun willDisplayErrorCorrectly() { given.gitHubServer.returnErrorOnFetchingRepositories() `when`.user.launchesTheApp() then.user.sees.repositoriesListScreenAssertion.withErrorState() } }
  127. mainScreen { setReturnResposListPageOneSuccessfully() launchTheApp() repositoriesListScreenIsCorrectlyLoaded }

  128. sourceSets { String sharedTestDir = 'src/sharedTest/java' test { debug.assets.srcDirs +=

    files("$projectDir/schemas".toString()) debug.assets.srcDirs += files("$projectDir/assets".toString()) debug.assets.srcDirs += files("$projectDir/raw".toString()) java.srcDirs += 'src/androidTest/java' kotlin.srcDirs += 'src/androidTest/java' java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { assets.srcDirs += files("$projectDir/schemas".toString()) test.resources.srcDirs += 'src/test/resources' resources.srcDirs += ['src/test/resources'] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  129. sourceSets { String sharedTestDir = 'src/sharedTest/java' test { debug.assets.srcDirs +=

    files("$projectDir/schemas".toString()) debug.assets.srcDirs += files("$projectDir/assets".toString()) debug.assets.srcDirs += files("$projectDir/raw".toString()) java.srcDirs += 'src/androidTest/java' kotlin.srcDirs += 'src/androidTest/java' java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { assets.srcDirs += files("$projectDir/schemas".toString()) test.resources.srcDirs += 'src/test/resources' resources.srcDirs += ['src/test/resources'] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  130. sourceSets { String sharedTestDir = 'src/sharedTest/java' test { debug.assets.srcDirs +=

    files("$projectDir/schemas".toString()) debug.assets.srcDirs += files("$projectDir/assets".toString()) debug.assets.srcDirs += files("$projectDir/raw".toString()) java.srcDirs += 'src/androidTest/java' kotlin.srcDirs += 'src/androidTest/java' java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { assets.srcDirs += files("$projectDir/schemas".toString()) test.resources.srcDirs += 'src/test/resources' resources.srcDirs += ['src/test/resources'] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  131. sourceSets { String sharedTestDir = 'src/sharedTest/java' test { debug.assets.srcDirs +=

    files("$projectDir/schemas".toString()) debug.assets.srcDirs += files("$projectDir/assets".toString()) debug.assets.srcDirs += files("$projectDir/raw".toString()) java.srcDirs += 'src/androidTest/java' kotlin.srcDirs += 'src/androidTest/java' java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { assets.srcDirs += files("$projectDir/schemas".toString()) test.resources.srcDirs += 'src/test/resources' resources.srcDirs += ['src/test/resources'] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  132. Code examples

  133. Some tips…

  134. Some tips… Move instrumented test into unitTest folder…

  135. Using the shared test folder :

  136. Using the shared test folder : sourceSets { String sharedTestDir

    = 'src/sharedTest/java' test { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  137. Using the shared test folder : sourceSets { String sharedTestDir

    = 'src/sharedTest/java' test { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  138. Using the shared test folder : sourceSets { String sharedTestDir

    = 'src/sharedTest/java' test { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  139. Using the shared test folder : sourceSets { String sharedTestDir

    = 'src/sharedTest/java' test { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir test.resources.srcDirs += 'src/test/resources' } androidTest { [...] java.srcDirs += sharedTestDir kotlin.srcDirs += sharedTestDir } }
  140. If you have lot of build variants, in which manner

    specify which UI test should run into JVM? Mixing everything into one shared folder isn’t a good solution!
  141. If you have lot of build variants, in which manner

    specify which UI test should run into JVM? Mixing everything into one shared folder isn’t a good solution!
  142. Adding dynamically using a Gradle task could be a good

    option :
  143. Adding dynamically using a Gradle task could be a good

    option : ./gradlew sourceSets | grep src/androidTest'$variantName'
  144. Adding dynamically using a Gradle task could be a good

    option : ./gradlew sourceSets | grep src/androidTest'$variantName'
  145. Adding dynamically using a Gradle task could be a good

    option :
  146. sourceSets.all { sourceSet -> Matcher testTaskMatcher = patternOfUnitTestTask.matcher(sourceSet.name) if (testTaskMatcher.find())

    { def variantName = testTaskMatcher.group(1) println "Added androidTest sourceSets to $sourceSet.name (variant name = $ {variantName}" sourceSet.java.srcDirs += "src/androidTest${variantName}/java" sourceSet.kotlin.srcDirs += "src/androidTest${variantName}/java" } } Adding dynamically using a Gradle task could be a good option :
  147. sourceSets.all { sourceSet -> Matcher testTaskMatcher = patternOfUnitTestTask.matcher(sourceSet.name) if (testTaskMatcher.find())

    { def variantName = testTaskMatcher.group(1) println "Added androidTest sourceSets to $sourceSet.name (variant name = $ {variantName}" sourceSet.java.srcDirs += "src/androidTest${variantName}/java" sourceSet.kotlin.srcDirs += "src/androidTest${variantName}/java" } } Adding dynamically using a Gradle task could be a good option :
  148. sourceSets.all { sourceSet -> Matcher testTaskMatcher = patternOfUnitTestTask.matcher(sourceSet.name) if (testTaskMatcher.find())

    { def variantName = testTaskMatcher.group(1) println "Added androidTest sourceSets to $sourceSet.name (variant name = $ {variantName}" sourceSet.java.srcDirs += "src/androidTest${variantName}/java" sourceSet.kotlin.srcDirs += "src/androidTest${variantName}/java" } } Adding dynamically using a Gradle task could be a good option :
  149. sourceSets.all { sourceSet -> Matcher testTaskMatcher = patternOfUnitTestTask.matcher(sourceSet.name) if (testTaskMatcher.find())

    { def variantName = testTaskMatcher.group(1) println "Added androidTest sourceSets to $sourceSet.name (variant name = $ {variantName}" sourceSet.java.srcDirs += "src/androidTest${variantName}/java" sourceSet.kotlin.srcDirs += "src/androidTest${variantName}/java" } } Adding dynamically using a Gradle task could be a good option :
  150. You can use Junit 4 ‘categories’ to exclude tests which

    for some reason has some incompatibilities with JVM:
  151. You can use Junit 4 ‘categories’ to exclude tests which

    for some reason has some incompatibilities with JVM: testOptions { [...] unitTests.all { [...] useJUnit { //With this category we can annotate tests will not run on JVM //due conflicts or other Robolectric Issue, but can run without any issue on Espresso or //normal device. excludeCategories 'com.playground.utils.DoNotRunOnJvm' } [...] } }
  152. You can use Junit 4 ‘categories’ to exclude tests which

    for some reason has some incompatibilities with JVM: testOptions { [...] unitTests.all { [...] useJUnit { //With this category we can annotate tests will not run on JVM //due conflicts or other Robolectric Issue, but can run without any issue on Espresso or //normal device. excludeCategories 'com.playground.utils.DoNotRunOnJvm' } [...] } }
  153. /** * Marks a test class or test method as

    belonging to one or more categories of tests. * The value is an array of arbitrary classes. * * This annotation is only interpreted by the Categories runner (at present). * * For example: * <pre> * public interface FastTests {} * public interface SlowTests {} * * public static class A { * &#064;Test * public void a() { * fail(); * } * * &#064;Category(SlowTests.class) * &#064;Test * public void b() { * } * } * * &#064;Category({SlowTests.class, FastTests.class}) * public static class B { * &#064;Test * public void c() { * * } * } * </pre> * * For more usage, see code example on {@link Categories}. */ @Retention(RetentionPolicy.RUNTIME) @Inherited @ValidateWith(CategoryValidator.class) public @interface Category { Class<?>[] value(); }
  154. @Category(DoNotRunOnJvm::class) class AirportPickerUiTest : BaseHolidaySearchUiTest() { [...] @Before override fun

    setUp() { super.setUp() willOpenHolidaySearchScreenCorrectly() } @Test fun willOpenAirportPickerScreenCorrectly() { `when`.user.apply { onBottomNavigation.clicksSearch() onHolidaySearchActions.clickAirportPicker() } then.user.sees.apply { airportPickerAssertions.withFields() airportPickerAssertions.withSuccessfullyLoadedFlightsFrom() } } [...] }
  155. @Category(DoNotRunOnJvm::class) class AirportPickerUiTest : BaseHolidaySearchUiTest() { [...] @Before override fun

    setUp() { super.setUp() willOpenHolidaySearchScreenCorrectly() } @Test fun willOpenAirportPickerScreenCorrectly() { `when`.user.apply { onBottomNavigation.clicksSearch() onHolidaySearchActions.clickAirportPicker() } then.user.sees.apply { airportPickerAssertions.withFields() airportPickerAssertions.withSuccessfullyLoadedFlightsFrom() } } [...] }
  156. Create a custom filter to check if our test has

    custom @TestFilter(flavors = [FLAVOR_1]) annotation. Based on that and the flavour name it check if we need to run it or no.
  157. Create a custom filter to check if our test has

    custom @TestFilter(flavors = [FLAVOR_1]) annotation. Based on that and the flavour name it check if we need to run it or no. @Test @TestFilter(flavors = [FLAVOR_1]) fun test1() { //... } @Test fun test2() { //... } @Test @TestFilter(flavors = [FLAVOR_2]) fun test3() { //... }
  158. Create a custom filter to check if our test has

    custom @TestFilter(flavors = [FLAVOR_1]) annotation. Based on that and the flavour name it check if we need to run it or no. @Test @TestFilter(flavors = [FLAVOR_1]) fun test1() { //... } @Test fun test2() { //... } @Test @TestFilter(flavors = [FLAVOR_2]) fun test3() { //... } https://medium.com/stepstone-tech/taming-ui-tests-when-using-multiple-gradle-flavors-6190b3692491
  159. Create a custom filter to check if our test has

    custom @TestFilter(flavors = [FLAVOR_1]) annotation. Based on that and the flavour name it check if we need to run it or no. @Test @TestFilter(flavors = [FLAVOR_1]) fun test1() { //... } @Test fun test2() { //... } @Test @TestFilter(flavors = [FLAVOR_2]) fun test3() { //... } https://medium.com/stepstone-tech/taming-ui-tests-when-using-multiple-gradle-flavors-6190b3692491
  160. Create a custom filter to check if our test has

    custom @TestFilter(flavors = [FLAVOR_1]) annotation. Based on that and the flavour name it check if we need to run it or no. @Test @TestFilter(flavors = [FLAVOR_1]) fun test1() { //... } @Test fun test2() { //... } @Test @TestFilter(flavors = [FLAVOR_2]) fun test3() { //... } https://medium.com/stepstone-tech/taming-ui-tests-when-using-multiple-gradle-flavors-6190b3692491
  161. Create a custom filter to check if our test has

    custom @TestFilter(flavors = [FLAVOR_1]) annotation. Based on that and the flavour name it check if we need to run it or no. @Test @TestFilter(flavors = [FLAVOR_1]) fun test1() { //... } @Test fun test2() { //... } @Test @TestFilter(flavors = [FLAVOR_2]) fun test3() { //... } https://medium.com/stepstone-tech/taming-ui-tests-when-using-multiple-gradle-flavors-6190b3692491
  162. class FlavorFilter(bundle: Bundle) : ParentFilter() { companion object { private

    const val CLASS_BUNDLE_KEY = "class" } private val shouldFilterTests: Boolean init { Timber.i("FlavorFilter bundle: %s", bundle) val listTestsForOrchestrator = java.lang.Boolean.parseBoolean(bundle.getString("listTestsForOrchestrator", "false")) val runsSingleClass = bundle.containsKey(CLASS_BUNDLE_KEY) shouldFilterTests = listTestsForOrchestrator && !runsSingleClass } override fun evaluateTest(description: Description): Boolean { if (!shouldFilterTests) { return true } val classTestFilter = description.testClass.getAnnotation(TestFilter::class.java) val testFilter = description.getAnnotation(TestFilter::class.java) if (testFilter != null) { return evaluateTestWithFilter(testFilter) } else if (classTestFilter != null) { return evaluateTestWithFilter(classTestFilter) } return true } private fun evaluateTestWithFilter(testFilter: TestFilter): Boolean { val filters = testFilter.filters val notFilters = testFilter.notFilters return (filters.contains(BuildConfig.FLAVOR) && !notFilters.contains(BuildConfig.FLAVOR)) } override fun describe(): String { return "Flavor filter" } }
  163. class FlavorFilter(bundle: Bundle) : ParentFilter() { companion object { private

    const val CLASS_BUNDLE_KEY = "class" } private val shouldFilterTests: Boolean init { Timber.i("FlavorFilter bundle: %s", bundle) val listTestsForOrchestrator = java.lang.Boolean.parseBoolean(bundle.getString("listTestsForOrchestrator", "false")) val runsSingleClass = bundle.containsKey(CLASS_BUNDLE_KEY) shouldFilterTests = listTestsForOrchestrator && !runsSingleClass } override fun evaluateTest(description: Description): Boolean { if (!shouldFilterTests) { return true } val classTestFilter = description.testClass.getAnnotation(TestFilter::class.java) val testFilter = description.getAnnotation(TestFilter::class.java) if (testFilter != null) { return evaluateTestWithFilter(testFilter) } else if (classTestFilter != null) { return evaluateTestWithFilter(classTestFilter) } return true } private fun evaluateTestWithFilter(testFilter: TestFilter): Boolean { val filters = testFilter.filters val notFilters = testFilter.notFilters return (filters.contains(BuildConfig.FLAVOR) && !notFilters.contains(BuildConfig.FLAVOR)) } override fun describe(): String { return "Flavor filter" } }
  164. class FlavorFilter(bundle: Bundle) : ParentFilter() { companion object { private

    const val CLASS_BUNDLE_KEY = "class" } private val shouldFilterTests: Boolean init { Timber.i("FlavorFilter bundle: %s", bundle) val listTestsForOrchestrator = java.lang.Boolean.parseBoolean(bundle.getString("listTestsForOrchestrator", "false")) val runsSingleClass = bundle.containsKey(CLASS_BUNDLE_KEY) shouldFilterTests = listTestsForOrchestrator && !runsSingleClass } override fun evaluateTest(description: Description): Boolean { if (!shouldFilterTests) { return true } val classTestFilter = description.testClass.getAnnotation(TestFilter::class.java) val testFilter = description.getAnnotation(TestFilter::class.java) if (testFilter != null) { return evaluateTestWithFilter(testFilter) } else if (classTestFilter != null) { return evaluateTestWithFilter(classTestFilter) } return true } private fun evaluateTestWithFilter(testFilter: TestFilter): Boolean { val filters = testFilter.filters val notFilters = testFilter.notFilters return (filters.contains(BuildConfig.FLAVOR) && !notFilters.contains(BuildConfig.FLAVOR)) } override fun describe(): String { return "Flavor filter" } }
  165. class FlavorFilter(bundle: Bundle) : ParentFilter() { companion object { private

    const val CLASS_BUNDLE_KEY = "class" } private val shouldFilterTests: Boolean init { Timber.i("FlavorFilter bundle: %s", bundle) val listTestsForOrchestrator = java.lang.Boolean.parseBoolean(bundle.getString("listTestsForOrchestrator", "false")) val runsSingleClass = bundle.containsKey(CLASS_BUNDLE_KEY) shouldFilterTests = listTestsForOrchestrator && !runsSingleClass } override fun evaluateTest(description: Description): Boolean { if (!shouldFilterTests) { return true } val classTestFilter = description.testClass.getAnnotation(TestFilter::class.java) val testFilter = description.getAnnotation(TestFilter::class.java) if (testFilter != null) { return evaluateTestWithFilter(testFilter) } else if (classTestFilter != null) { return evaluateTestWithFilter(classTestFilter) } return true } private fun evaluateTestWithFilter(testFilter: TestFilter): Boolean { val filters = testFilter.filters val notFilters = testFilter.notFilters return (filters.contains(BuildConfig.FLAVOR) && !notFilters.contains(BuildConfig.FLAVOR)) } override fun describe(): String { return "Flavor filter" } }
  166. None
  167. Using @TestFilter(flavors = [FLAVOR_1]) annotation 1- It is easy to

    figure out which test gets run on which flavors as that information is right above the test method. 2- Adding new tests does not require updating any external configuration files. 3- All tests are in androidTest folder so it’s easier to navigate through them.
  168. How to debug a JVM test which runs by command

    line ?
  169. How to debug a JVM test which runs by command

    line ? ./gradlew test'$buildVariantName'Test - Dorg.gradle.daemon=false --tests '$package.className' -- debug-jvm 'Listening for transport dt_socket at address: 5005' export GRADLE_OPTS="- agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=500 5"
  170. How to debug a JVM test which runs by command

    line ? ./gradlew test'$buildVariantName'Test - Dorg.gradle.daemon=false --tests '$package.className' -- debug-jvm 'Listening for transport dt_socket at address: 5005' export GRADLE_OPTS="- agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=500 5"
  171. Use test class level qualifiers

  172. Use test class level qualifiers @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application

    = TestTdaApplication::class) abstract class RobolectricTestBase {
  173. Use test class level qualifiers @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application

    = TestTdaApplication::class) abstract class RobolectricTestBase {
  174. …better Robolectric.properties

  175. …better Robolectric.properties application = com.playground.base.MvRxTestApplication qualifiers = en-rGB-w360dp-h640dp-xhdpi shadows =

    com.xxx.shadowedClass sdk=28
  176. …better Robolectric.properties application = com.playground.base.MvRxTestApplication qualifiers = en-rGB-w360dp-h640dp-xhdpi shadows =

    com.xxx.shadowedClass sdk=28
  177. Use rules to clean your env! /** * Clear all

    application data here. * Done to ensure all UI tests start from the same clean state. */ class ClearAppDataRule : ExternalResource() { private val db: MvRxDb by ApplicationProvider.getApplicationContext<MvRxTestApplication>().inject() override fun before() { if (SystemUtils.isART()) { db.clearAllTables() } super.before() } }
  178. Robolectric open issues…

  179. @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application = TestTdaApplication::class) abstract class RobolectricTestBase

    { private var app: Application? = null companion object { @JvmStatic @BeforeClass fun beforeClass() { // This forces the usage of com.almworks.sqlite4java:1.0.392, which internally uses SQLite 3.8.7. // This version of SQLite is new enough to support all SQLite features we use SQLiteLibraryLoader.load() } } @Before @CallSuper open fun setUp() { MockitoAnnotations.initMocks(this) app = ApplicationProvider.getApplicationContext() } } …are still a lot…
  180. @RunWith(RobolectricTestRunner::class) @Config(sdk = [21], application = TestTdaApplication::class) abstract class RobolectricTestBase

    { private var app: Application? = null companion object { @JvmStatic @BeforeClass fun beforeClass() { // This forces the usage of com.almworks.sqlite4java:1.0.392, which internally uses SQLite 3.8.7. // This version of SQLite is new enough to support all SQLite features we use SQLiteLibraryLoader.load() } } @Before @CallSuper open fun setUp() { MockitoAnnotations.initMocks(this) app = ApplicationProvider.getApplicationContext() } } …are still a lot…
  181. Use shadow classes !

  182. Use shadow classes ! Why “Shadow?” Shadow objects are not

    quite Proxies, not quite Fakes, not quite Mocks or Stubs. Shadows are sometimes hidden, sometimes seen, and can lead you to the real object. At least we didn’t call them “sheep”, which we were considering.
  183. …to avoid lot of JVM incompatibilities !

  184. …to avoid lot of JVM incompatibilities ! package com.tui.tda.shadows import

    android.util.Log import com.tui.tda.components.messages.fragments.MessagesOnlineDetai lsFragment import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements @Implements(MessagesOnlineDetailsFragment::class) class MessagesOnlineDetailsFragmentShadow { /** * Run SoomthScrollPosition isn't working in Roboelectric, so we need * to shadow the class in this case. */ @Implementation fun smoothScrollToPosition() { Log.e(MessagesOnlineDetailsFragmentShadow::class.java.name, "smoothScrollToPosition() shadow method due https:// github.com/robolectric/robolectric/issues/3552") } }
  185. …to avoid lot of JVM incompatibilities ! package com.tui.tda.shadows import

    android.util.Log import com.tui.tda.components.messages.fragments.MessagesOnlineDetai lsFragment import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements @Implements(MessagesOnlineDetailsFragment::class) class MessagesOnlineDetailsFragmentShadow { /** * Run SoomthScrollPosition isn't working in Roboelectric, so we need * to shadow the class in this case. */ @Implementation fun smoothScrollToPosition() { Log.e(MessagesOnlineDetailsFragmentShadow::class.java.name, "smoothScrollToPosition() shadow method due https:// github.com/robolectric/robolectric/issues/3552") } }
  186. …to avoid lot of JVM incompatibilities ! package com.tui.tda.shadows import

    android.util.Log import com.tui.tda.components.messages.fragments.MessagesOnlineDetai lsFragment import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements @Implements(MessagesOnlineDetailsFragment::class) class MessagesOnlineDetailsFragmentShadow { /** * Run SoomthScrollPosition isn't working in Roboelectric, so we need * to shadow the class in this case. */ @Implementation fun smoothScrollToPosition() { Log.e(MessagesOnlineDetailsFragmentShadow::class.java.name, "smoothScrollToPosition() shadow method due https:// github.com/robolectric/robolectric/issues/3552") } }
  187. What to expect :

  188. What to expect :

  189. References : Testing Rebooted (with AndroidX Test) (Android Dev Summit

    '18) Frictionless Android testing: write once, run everywhere (Google I/O '18) Do Android devs dream of Robolectric Espresso? by Sarah Sharp (The Telegraph) Testing Android Apps at Scale with Nitrogen (Android Dev Summit '18) Android Test documentation How to remote debug a Robolectric test by command line using Gradle Code examples : https://github.com/lupsyn/mvRx_playground
  190. Thanks! @lupsyn @tamasmarton13 Enrico Bruno Del Zotto Tamas Marton