Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Robolectricの限界を理解してUIテストを高速に実行しよう / Let's run UI Test faster with understanding limit of Robolectric

Robolectricの限界を理解してUIテストを高速に実行しよう / Let's run UI Test faster with understanding limit of Robolectric

DroidKaigi 2020で発表予定だった資料「Robolectricの限界を理解してUIテストを高速に実行しよう」のスライドです。
DeNA.apk #2【カンファレンス中止でも登壇はしたい】Day2」でライブ配信します。

サンプルコードURL: https://github.com/sumio/robolectric-espresso-samples

TOYAMA Sumio

March 16, 2020
Tweet

More Decks by TOYAMA Sumio

Other Decks in Programming

Transcript

  1. $ 2 p :  # (TOYAMA Sumio) @sumio_tym (Twitter)

    / @sumio (GitHub) p  : DeNA SWET  (Software Engineer in Test) p :  p : Android "! https://peaks.cc/sumio_tym/android_testing
  2. )  3 Espresso-  Robolectric'   /% 

    p Robolectric!'* p Robolectric.+1$0 " #& p Robolectric'(,'(, p Robolectric'  UI
  3. %) 4 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  4. %) 5 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  5.    6 This image is reproduced from work

    (https://d.android.com/training/testing/fundamentals) created and shared by the Android Open Source Project (https://d.android.com/license) and used according to terms described in the Creative Commons 2.5 Attribution License (https://creativecommons.org/licenses/by/2.5/).
  6.    7 This image is reproduced from work

    (https://d.android.com/training/testing/fundamentals) created and shared by the Android Open Source Project (https://d.android.com/license) and used according to terms described in the Creative Commons 2.5 Attribution License (https://creativecommons.org/licenses/by/2.5/). Local Test PC Instrumented Test Android     UI Tests   Integration Tests Unit Tests  
  7. Local TestInstrumented Test  8 Local Test Instrumented Test 

     PC (JVM) Android    Android API   (Espresso API)       src/test src/androidTest
  8. Local Test Instrumented Test  9 Local Test Instrumented Test

      PC (JVM) Android    Android API    (Espresso API)        src/test src/androidTest   
  9. +25-Robolectric 10 p JVM# Android !',   (http://robolectric.org/) p

    *&!: 4.3.1 p 3rd party3/ Android)0  p $Instrumented/Local Test1% (   Robolectric.4"(Project Nitrogen) () () Copyright 6 2010 Xtreme Labs, Pivotal Labs and Google Inc. This logo image (https://github.com/robolectric/robolectric/blob/master/images/robolectric-horizontal.png) is licensed under The MIT License (https://github.com/robolectric/robolectric/blob/master/LICENSE).
  10. $*-% Robolectric 11 p JVMAndroid   (http://robolectric.org/) p #

     : 4.3.1 p 3rd party +' Android "( p  Instrumented/Local Test ) !   Robolectric &, (Project Nitrogen) () () Copyright . 2010 Xtreme Labs, Pivotal Labs and Google Inc. This logo image (https://github.com/robolectric/robolectric/blob/master/images/robolectric-horizontal.png) is licensed under The MIT License (https://github.com/robolectric/robolectric/blob/master/LICENSE).
  11. Local TestRobolectric 3.x   12 Local Test /w Robolectric

    3.x Instrumented Test   PC (JVM) Android    Android API   (Espresso API)        src/test src/androidTest
  12. Local TestRobolectric 4.x  13 Local Test /w Robolectric 4.x

    Instrumented Test    PC (JVM) Android    Android API   (Espresso API)          src/test src/androidTest
  13. Robolectric 4.x  14 Local Test UI Test  

    p   UI Test   p UI Test    (  ) This image is a modification based on work (https://d.android.com/training/testing/fundamentals ) created and shared by the Android Open Source Project (https://d.android.com/license) and used according to terms described in the Creative Commons 2.5 Attribution License (https://creativecommons.org/licenses/by/2.5/).
  14. Robolectric 4.xEspresso Support*1 15 p Robolectric 4.x Espresso Support p

    74UI Test ).   p Robolectric32( ) 5+ %/ p "!$0 p #!&6-'  ,
  15. Robolectric 4.x(  16 p Robolectric  &  p

    *% "'  $&  p Robolectric! (#))   p Robolectric(#))  $,- +  
  16. %) 17 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  17. Robolectric (05) 18 p JVM% Android ! #)- !" (http://robolectric.org/)

    p ,( #: 4.3.1 p 3rd party3/Android+1!" p &Instrumented/Local Test2' *  Robolectric.4$(Project Nitrogen) () () Copyright 6 2010 Xtreme Labs, Pivotal Labs and Google Inc. This logo image (https://github.com/robolectric/robolectric/blob/master/images/robolectric-horizontal.png) is licensed under The MIT License (https://github.com/robolectric/robolectric/blob/master/LICENSE).
  18. Robolectric: Android! ,) 19 1. Android (jar)!  https://mvnrepository.com/artifact/org.robolectric/android-all 2.

    ! Android%- 3. %#$ *    p Robolectric&'Shadow   Shadow  p Shadow+ "(  
  19. Robolectric: Shadow   20 p Android +  #+$(

    p native*  p Android*  p etc. p " , API !& p ': logcat  )% ShadowLog.stream = System.out
  20. Robolectric: 2  (0/ .6 21 ( @Before p 4'#!%"

    onCreate1 p static$ (+&  ,-) p JVM5 SQLite(3(sqlite4java))* p  $ ( )
  21. Robolectric:  ! 22    (LEGACY) p 

       (  ) p   (AsyncTask ) (  )
  22. Robolectric: LEGACY# PAUSED# 24 Looper Mode46Robolectric4.3PAUSED2' LEGACY p $!(/0& 

    5+ p )-"%* 3  UI   , PAUSED p )-"%* .1  p EspressoUI 
  23. Robolectric: LEGACY(%PAUSED(% 25 Looper Mode<?Robolectric4.3 PAUSED9, LEGACY p )&"$#!$-67+=1 p

    .3 '*/ ; UI    2 PAUSED p .3 '*/ 58  p EspressoUI#!$   0> PAUSED 4:
  24. Robolectric:    26 p Robolectric "( p JVM!

    Android ! p Android & Shadow )+ p $"  p Espresso PAUSED Looper Mode p    #'% &"  p '% $(,*$-   p   # 
  25. %) 27 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  26.   : build.gradle 28 android { ... testOptions {

    unitTests.includeAndroidResources = true } } Robolectric 
  27.  : () 29    Latest Stable Version()

    androidx.test:core-ktx 1.2.0 androidx.test.ext:junit-ktx 1.1.1 androidx.test.espresso:espresso-contrib 3.2.0 org.robolectric:robolectric 4.3.1  testImplementation  ()2020/03/01
  28.  :    31 @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class MyRobolectricTest

    { @get:Rule val activityScenarioRule = activityScenarioRule<MyActivity>() ... } PAUSED Looper Mode  ActivityTestRule
  29.  :  32 p targetSdkVersion29JDK9 p JDK8Robolectric 28 @RunWith(AndroidJUnit4::class)

    @LooperMode(LooperMode.Mode.PAUSED) @Config(sdk = 28) class MyRobolectricTest { ... }
  30. :    33 p Local TestRobolectricEspresso  OK

    p  p Instrumented Test  p PAUSED Looper Mode p ActivityTestScenario  p   SDK  
  31. %) 34 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  32. 9' : Espresso(.)0/8*57; 35 Instrumented Robolectric $&% #  

    AsyncTask   IdlingResource   <)06- 31>+=?9   Espresso(.)0/8 A DroidKaigi 2018,2  Espresso!"#)06-:( http://bit.ly/2QdCeBt )4@  
  33. : Robolectric   36 !1: View '(  

    p +  p Instrumented Test  $#   !2: RobolectricEspresso " & p Instrumented Test  $#  p Espresso ,*(OSS)  p Robolectric )%
  34. - % : Robolectric- %  37 !1: View '(

      p +'( p Instrumented Test !$#   !2: RobolectricEspresso " & p Instrumented Test !$#  p Espresso*,*(OSS)+) p Robolectric&, )% $"# '(/.  
  35. :    38 UiController   loopMainThreadUntilIdle() 

    p      META-INF/services/ androidx.test.platform.ui.UiController p Espresso : UiControllerImpl p Robolectric : LocalUiController
  36. : Espresso  40 UiControllerImpl.loopMainThreadUntilIdle() ... do { if (!asyncIdle.isIdleNow())

    { ... } if (!compatIdle.isIdleNow()) { ... } if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); } while (!asyncIdle.isIdleNow() || !compatIdle.isIdleNow() || !dynamicIdle.isIdleNow());
  37.  : Espresso   41 ... do { if

    (!asyncIdle.isIdleNow()) { ... } if (!compatIdle.isIdleNow()) { ... } if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); } while (!asyncIdle.isIdleNow() || !compatIdle.isIdleNow() || !dynamicIdle.isIdleNow());     UiControllerImpl.loopMainThreadUntilIdle()
  38. :  loopMain...  42 class IdlingLocalUiController extends LocalUiController {

    ... public void loopMainThreadUntilIdle() { ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } }
  39. :  loopMain...  43 class IdlingLocalUiController extends LocalUiController {

    ... public void loopMainThreadUntilIdle() { ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } Robolectric  
  40. class IdlingLocalUiController extends LocalUiController { ... public void loopMainThreadUntilIdle() {

    ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } :  loopMain...   44 loopMainThreadUntilIdle  
  41. class IdlingLocalUiController extends LocalUiController { ... public void loopMainThreadUntilIdle() {

    ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } }  :  loopMain...   45 Espresso dynamicIdle 
  42. class IdlingLocalUiController extends LocalUiController { ... public void loopMainThreadUntilIdle() {

    ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } : loopMain... 46 1
  43. :     47 robolectric-4.3.1.jar  META-INF/services/ androidx.test.platform.ui.UiController

    - org.robolectric.android.internal.LocalUiController + androidx.test.espresso.base.IdlingLocalUiController
  44.  :    49 - final Message m

    = getNextMessage(); + Duration nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask();
  45.  :    50 - final Message m

    = getNextMessage(); + Duration nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask();   
  46. (: 51 - final Message m = getNextMessage(); + Duration

    nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask(); ) 
  47. : ) 52 - final Message m = getNextMessage(); +

    Duration nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask(); )   (
  48. 5 %: (1)+9 */.- 53  sleep ( ) 

     p (4!# %    $:  3"!#  1.  2,78 Robolectric 603-  2. Espresso 5 %  ( $603) 3. " AppNotIdleException &'
  49. A.": /?04O  54  E8 https://git.io/Jv2ej >K (commit B"

    <) (1)Robolectric*9Pull Request:2)  MD04,36@-J   LH http://bit.ly/2TSsKxQ #>K  +;URL/?04N 5I%(&CG *&($'=F!0457
  50. +: * !# 55 p https://git.io/Jv2ej ,-% p & $'

    .  README" IdlingResource ) Robolectric*   (/ ( http://bit.ly/2IKBGPD )
  51. 0 # : /+ 56 p Instrumented Test),  IdlingResource

      p AsyncTask&. %#: ShadowPausedAsyncTask.overrideExecutor IdlingThreadPoolExecutor*' p UI AutomatorUiDevice.wait()  1$!" -(
  52. 8' :  57 p RobolectricEspresso3 $&% #4< 8' 

    p Robolectric/9  IdlingResource(671 p ;+):-*0 https://git.io/Jv2ej 5= p Instrumented Test.2 IdlingResource3 Espresso!"#, p AsyncTask(671
  53. %) 58 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  54. Jetpack:  59 Robolectric UI   Robolectric ↓ Android

    Architecture Component 20197 by Nozomi Takuma @ Android Test Night #7 (2019/07/25) http://bit.ly/32YCauI
  55. Jetpack: JetpackRobolectric   60  Jetpack   p

    LiveData, Room, WorkManager, etc.  Robolectric      p Room, WorkManager
  56. Jetpack:    61 p RobolectricIdlingResource   Instrumented

    Test   p 2 p TaskExecutorWithIdlingResourceRule p DataBindingIdlingResource
  57. Jetpack :    62 TaskExecutorWithIdlingResourceRule https://git.io/Jv2Ud p ArchTaskExecutor(AAC

    )   p    @get:Rule val executorRule = TaskExecutorWithIdlingResourceRule()
  58. Jetpack :   -1 63 DataBindingIdlingResource https://git.io/Jv2UF p Data

    Binding   (Fragment) p DataBinding  
  59. Jetpack :    -2 64 val dbIdlingResource =

    DataBindingIdlingResource() @Before fun setUp() { val scenario = activityScenarioRule.scenario dbIdlingResource.monitorActivity(scenario) IdlingRegistry.getInstance() .register(dbIndlingResource) } @After fun tearDown() { IdlingRegistry.getInstance() .unregister(dbIndlingResource) }
  60. Jetpack:  -3 65 val dbIdlingResource = DataBindingIdlingResource() @Before fun

    setUp() { val scenario = activityScenarioRule.scenario dbIdlingResource.monitorActivity(scenario) IdlingRegistry.getInstance() .register(dbIndlingResource) } @After fun tearDown() { IdlingRegistry.getInstance() .unregister(dbIndlingResource) }     
  61. Jetpack: $!&-4 66 val dbIdlingResource = DataBindingIdlingResource() @Before fun setUp()

    { val scenario = activityScenarioRule.scenario dbIdlingResource.monitorActivity(scenario) IdlingRegistry.getInstance() .register(dbIndlingResource) } @After fun tearDown() { IdlingRegistry.getInstance() .unregister(dbIndlingResource) }   # "%
  62. Jetpack: Robolectric / 67   OK   DataBinding

      LifeCycle   LiveData   Navigation (NavigationView )  Paging (  )  Room   ViewModel   WorkManager   SavedState  
  63. Jetpack: Robolectric /  68   OK  

    DataBinding   LifeCycle   LiveData   Navigation (NavigationView )  Paging (  )  Room   ViewModel   WorkManager   SavedState      
  64. Jetpack: Room   69 : Robolectric D p 

    DB : p  static R p  Room.databaseBuilder() : p DBDAO  : 
  65. Jetpack : Room (Android Sunflower) 70 @Database(...) abstract class AppDatabase

    : RoomDatabase() { abstract fun plantDao(): PlantDao companion object { @Volatile private var instance: AppDatabase? = null fun getInstance(ctx: Context): AppDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(ctx).also { instance = it } } }  
  66. Jetpack: Room (Android Sunflower) 71 // companion object private fun

    buildDatabase(ctx: Context) = Room .databaseBuilder(ctx, AppDatabase::class.java, DATABASE_NAME)...build() @VisibleForTesting(otherwise = NONE) fun clear() { instance = null } ...
  67. Jetpack : Room (Android Sunflower) 72 // companion object private

    fun buildDatabase(ctx: Context) = Room .databaseBuilder(ctx, AppDatabase::class.java, DATABASE_NAME)...build() @VisibleForTesting(otherwise = NONE) fun clear() { instance = null } ...   )( )
  68. Jetpack : Room (Android Sunflower) 73 // B A class

    TestApplication : Application() { override fun onCreate() { ... val db = AppDatabase.getInstance(this) PlantRepository.updateDao(db.plantDao()) ... } } P D P O ) (
  69. Jetpack: Room(Android Sunflower) 74 //  @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application =

    TestApplication::class) class RobolectricGardenActivityTest2 { ... @After fun tearDown() { val appDatabase = AppDatabase.getInstance( ApplicationProvider.getApplicationContext()) appDatabase.close() AppDatabase.clear() } }
  70. Jetpack: Room!(Android Sunflower)# 75 //   @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application

    = TestApplication::class) class RobolectricGardenActivityTest2 { ... @After fun tearDown() { val appDatabase = AppDatabase.getInstance( ApplicationProvider.getApplicationContext()) appDatabase.close() AppDatabase.clear() } }    "    
  71. Jetpack".: WorkManager".  )0 76 ): WorkManager   p

    345Work% (& p static-&/,6(! #!: ' "$ p  '%$21  (http://bit.ly/332dPUP) src/test/AndroidManifest.xml *# 21 p WorkManagerTestInitHelper+ '%$  (http://bit.ly/2VVS5sk)
  72. Jetpack : WorkManager  77 // src/test/AndroidManifest.xml <manifest xmlns:android="..." xmlns:tools="...">

    <application> <provider android:name= "androidx.work.impl.WorkManagerInitializer" android:authorities= "${applicationId}.workmanager-init" tools:node="remove" /> </application> </manifest>  
  73. Jetpack#: WorkManager#%) 78 //   class TestApplication : Application()

    { override fun onCreate() { ... WorkManagerTestInitHelper .initializeTestWorkManager(this) ... } }   • &'(  "  •  ! $
  74. Jetpack08:  79 p Jetpack IdlingResource514  p RoomWorkManager:60872 p

    #!%09)+/3 p     p NavigationView ('( *.&,-)=4 p Paging5 RecyclerView=4 (<,&$",& ; )
  75. %) 80 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  76. Espresso API:  81 p withId() withText()  $ (!#

    %) p   click(), replaceText(), typeText(), scrollTo(), pressKey() p "  RecyclerViewActions
  77. Espresso API:   82 p DrawerActions openDrawer()  

     p NavigationViewActions  
  78. Espresso API:    83 p ! API2 p

    DrawerActions p NavigationViewActions p " (   )
  79. %) 84 p .+ p Robolectric  p Espresso& Local

    Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI
  80. !UI :   86 p  ' p 1Activity/Fragment,

    -$A(&B"%   p 1 hop (&.* -$A(&.* B"%   p     )# p Paging RecyclerView + 
  81. UI: Shared Test  87 Instrumented TestLocal Test  

    ? // app/build.gradle sourceSets { androidTest { java.srcDirs += file('src/sharedTest/java') } test { java.srcDirs += file('src/sharedTest/java') } }
  82. UI: Shared Test # 88 Instrumented Test Local Test 

      ? // app/build.gradle sourceSets { androidTest { java.srcDirs += file('src/sharedTest/java') } test { java.srcDirs += file('src/sharedTest/java') } } androidTest test !  src/sharedTest "
  83. UI: Shared Test  # 90 p src/sharedTest p AndroidAPIRobolectric"

      RobolectricAPI Android!  p EspressoAPI   p UI"src/sharedTest/ PageObject (http://bit.ly/2TzLLVF)  p  () ! (src/androidTest   src/test)
  84. !UI:   91 p  "$#.'   p

     )! p 1 Activity/Fragment-  p 1 hop *(/,  p Shared Test%& UI   p PageObject+!
  85. *( 93 Espresso1  ! Robolectric+ 2,87 p Robolectric 

    static3/DB#$% p IdlingResource1 6 & '4 p JetpackEspresso API0)5-. p  1 p RoomWorkManagerNavigationViewPaging5- p  "
  86. URL 94 p Improving Robolectric's Looper simulation http://robolectric.org/blog/2019/06/04/paused-looper/ p Issue

    #4870 IdlingResource doesn't seem to be working with Robolectric https://github.com/robolectric/robolectric/issues/4807 p Espresso, Beyond the basicsby Iñaki Villar in 360|AnDev 2017 http://bit.ly/2wUb2Bc p Shared Test      by ksfee684 in Cookpad.apk #4 http://bit.ly/2WlQ2Ot
  87. 3-URL ( 1:=,!() 95 p "2 UI Automator/  in

    DroidKaigi 2016 ( http://bit.ly/2TP3CYS ) p 2;+Espresso857. in DroidKaigi 2017 ( http://bit.ly/2TzLLVF ) p  Espresso#*6&9 in DroidKaigi 2018 ( http://bit.ly/2QdCeBt ) p EspressoAndroid)%$4  in DroidKaigi 2019 ( http://bit.ly/33lLwRp ) p .<Android'.(0=) https://peaks.cc/sumio_tym/android_testing