Share
Embed
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
Transcript
96 slides
-
Robolectric UI 2020.03.17 (DeNA.apk #2 Day
2) (sumio_tym) -
$ 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 -
) 3 Espresso- Robolectric' /%
p Robolectric!'* p Robolectric.+1$0 " #& p Robolectric'(,'(, p Robolectric' UI -
%) 4 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
%) 5 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
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/). -
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 -
Local TestInstrumented Test 8 Local Test Instrumented Test
PC (JVM) Android Android API (Espresso API) src/test src/androidTest -
Local Test Instrumented Test 9 Local Test Instrumented Test
PC (JVM) Android Android API (Espresso API) src/test src/androidTest -
+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). -
$*-% 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). -
Local TestRobolectric 3.x 12 Local Test /w Robolectric
3.x Instrumented Test PC (JVM) Android Android API (Espresso API) src/test src/androidTest -
Local TestRobolectric 4.x 13 Local Test /w Robolectric 4.x
Instrumented Test PC (JVM) Android Android API (Espresso API) src/test src/androidTest -
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/). -
Robolectric 4.xEspresso Support*1 15 p Robolectric 4.x Espresso Support p
74UI Test ). p Robolectric32( ) 5+ %/ p "!$0 p #!&6-' , -
Robolectric 4.x( 16 p Robolectric & p
*% "' $& p Robolectric! (#)) p Robolectric(#)) $,- + -
%) 17 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
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). -
Robolectric: Android! ,) 19 1. Android (jar)! https://mvnrepository.com/artifact/org.robolectric/android-all 2.
! Android%- 3. %#$ * p Robolectric&'Shadow Shadow p Shadow+ "( -
Robolectric: Shadow 20 p Android + #+$(
p native* p Android* p etc. p " , API !& p ': logcat )% ShadowLog.stream = System.out -
Robolectric: 2 (0/ .6 21 ( @Before p 4'#!%"
onCreate1 p static$ (+& ,-) p JVM5 SQLite(3(sqlite4java))* p $ ( ) -
Robolectric: ! 22 (LEGACY) p
( ) p (AsyncTask ) ( ) -
Robolectric: 23 &%(PAUSED) p !( p ShadowLooper.getShadowMainLooper().idle() p SystemClock
)' $ SystemClock.sleep()( * #" p !( ) -
Robolectric: LEGACY# PAUSED# 24 Looper Mode46Robolectric4.3PAUSED2' LEGACY p $!(/0&
5+ p )-"%* 3 UI , PAUSED p )-"%* .1 p EspressoUI -
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: -
Robolectric: 26 p Robolectric "( p JVM!
Android ! p Android & Shadow )+ p $" p Espresso PAUSED Looper Mode p #'% &" p '% $(,*$- p # -
%) 27 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
: build.gradle 28 android { ... testOptions {
unitTests.includeAndroidResources = true } } Robolectric -
: () 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 -
: 31 @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class MyRobolectricTest
{ @get:Rule val activityScenarioRule = activityScenarioRule<MyActivity>() ... } PAUSED Looper Mode ActivityTestRule -
: 32 p targetSdkVersion29JDK9 p JDK8Robolectric 28 @RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED) @Config(sdk = 28) class MyRobolectricTest { ... } -
: 33 p Local TestRobolectricEspresso OK
p p Instrumented Test p PAUSED Looper Mode p ActivityTestScenario p SDK -
%) 34 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
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@ -
: Robolectric 36 !1: View '(
p + p Instrumented Test $# !2: RobolectricEspresso " & p Instrumented Test $# p Espresso ,*(OSS) p Robolectric )% -
- % : Robolectric- % 37 !1: View '(
p +'( p Instrumented Test !$# !2: RobolectricEspresso " & p Instrumented Test !$# p Espresso*,*(OSS)+) p Robolectric&, )% $"# '(/. -
: 38 UiController loopMainThreadUntilIdle()
p META-INF/services/ androidx.test.platform.ui.UiController p Espresso : UiControllerImpl p Robolectric : LocalUiController -
: Robolectric 39 LocalUiController.loopMainThreadUntilIdle() @Override public void loopMainThreadUntilIdle() {
shadowMainLooper().idle(); } -
: Espresso 40 UiControllerImpl.loopMainThreadUntilIdle() ... do { if (!asyncIdle.isIdleNow())
{ ... } if (!compatIdle.isIdleNow()) { ... } if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); } while (!asyncIdle.isIdleNow() || !compatIdle.isIdleNow() || !dynamicIdle.isIdleNow()); -
: Espresso 41 ... do { if
(!asyncIdle.isIdleNow()) { ... } if (!compatIdle.isIdleNow()) { ... } if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); } while (!asyncIdle.isIdleNow() || !compatIdle.isIdleNow() || !dynamicIdle.isIdleNow()); UiControllerImpl.loopMainThreadUntilIdle() -
: loopMain... 42 class IdlingLocalUiController extends LocalUiController {
... public void loopMainThreadUntilIdle() { ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } -
: loopMain... 43 class IdlingLocalUiController extends LocalUiController {
... public void loopMainThreadUntilIdle() { ... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } Robolectric -
class IdlingLocalUiController extends LocalUiController { ... public void loopMainThreadUntilIdle() {
... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } : loopMain... 44 loopMainThreadUntilIdle -
class IdlingLocalUiController extends LocalUiController { ... public void loopMainThreadUntilIdle() {
... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } : loopMain... 45 Espresso dynamicIdle -
class IdlingLocalUiController extends LocalUiController { ... public void loopMainThreadUntilIdle() {
... do { ... if (!dynamicIdle.isIdleNow()) { ... } dynamicIdle = loopUntil(..., dynamicIdle); ShadowLooper.shadowMainLooper().idle(); } while (!dynamicIdle.isIdleNow()); } } : loopMain... 46 1 -
: 47 robolectric-4.3.1.jar META-INF/services/ androidx.test.platform.ui.UiController
- org.robolectric.android.internal.LocalUiController + androidx.test.espresso.base.IdlingLocalUiController -
- org.robolectric.android.internal.LocalUiController + androidx.test.espresso.base.IdlingLocalUiController : . 48 robolectric-4.3.1.jar META-INF/services/
androidx.test.platform.ui.UiController . -
: 49 - final Message m
= getNextMessage(); + Duration nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask(); -
: 50 - final Message m
= getNextMessage(); + Duration nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask(); -
(: 51 - final Message m = getNextMessage(); + Duration
nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask(); ) -
: ) 52 - final Message m = getNextMessage(); +
Duration nextScheduledTaskTime = + shadowOf(Looper.myLooper()) + .getNextScheduledTaskTime(); ... - m.getTarget().dispatchMessage(m); + Thread.sleep(nextScheduledTaskTime.toMillis()); + shadowOf(Looper.myLooper()).runToNextTask(); ) ( -
5 %: (1)+9 */.- 53 sleep ( )
p (4!# % $: 3"!# 1. 2,78 Robolectric 603- 2. Espresso 5 % ( $603) 3. " AppNotIdleException &' -
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 -
+: * !# 55 p https://git.io/Jv2ej ,-% p & $'
. README" IdlingResource ) Robolectric* (/ ( http://bit.ly/2IKBGPD ) -
0 # : /+ 56 p Instrumented Test), IdlingResource
p AsyncTask&. %#: ShadowPausedAsyncTask.overrideExecutor IdlingThreadPoolExecutor*' p UI AutomatorUiDevice.wait() 1$!" -( -
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 -
%) 58 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
Jetpack: 59 Robolectric UI Robolectric ↓ Android
Architecture Component 20197 by Nozomi Takuma @ Android Test Night #7 (2019/07/25) http://bit.ly/32YCauI -
Jetpack: JetpackRobolectric 60 Jetpack p
LiveData, Room, WorkManager, etc. Robolectric p Room, WorkManager -
Jetpack: 61 p RobolectricIdlingResource Instrumented
Test p 2 p TaskExecutorWithIdlingResourceRule p DataBindingIdlingResource -
Jetpack : 62 TaskExecutorWithIdlingResourceRule https://git.io/Jv2Ud p ArchTaskExecutor(AAC
) p @get:Rule val executorRule = TaskExecutorWithIdlingResourceRule() -
Jetpack : -1 63 DataBindingIdlingResource https://git.io/Jv2UF p Data
Binding (Fragment) p DataBinding -
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) } -
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) } -
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) } # "% -
Jetpack: Robolectric / 67 OK DataBinding
LifeCycle LiveData Navigation (NavigationView ) Paging ( ) Room ViewModel WorkManager SavedState -
Jetpack: Robolectric / 68 OK
DataBinding LifeCycle LiveData Navigation (NavigationView ) Paging ( ) Room ViewModel WorkManager SavedState -
Jetpack: Room 69 : Robolectric D p
DB : p static R p Room.databaseBuilder() : p DBDAO : -
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 } } } -
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 } ... -
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 } ... )( ) -
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 ) ( -
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() } } -
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() } } " -
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) -
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> -
Jetpack#: WorkManager#%) 78 // class TestApplication : Application()
{ override fun onCreate() { ... WorkManagerTestInitHelper .initializeTestWorkManager(this) ... } } • &'( " • ! $ -
Jetpack08: 79 p Jetpack IdlingResource514 p RoomWorkManager:60872 p
#!%09)+/3 p p NavigationView ('( *.&,-)=4 p Paging5 RecyclerView=4 (<,&$",& ; ) -
%) 80 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
Espresso API: 81 p withId() withText() $ (!#
%) p click(), replaceText(), typeText(), scrollTo(), pressKey() p " RecyclerViewActions -
Espresso API: 82 p DrawerActions openDrawer()
p NavigationViewActions -
Espresso API: 83 p ! API2 p
DrawerActions p NavigationViewActions p " ( ) -
%) 84 p .+ p Robolectric p Espresso& Local
Test p Espresso- "* ( p ,$'#Android Jetpack Components p ,$'#Espresso API p Robolectric !UI -
!UI: Robolectric-6 85 p /7(4 .+) p NavigationView"&$%
:0 9 p #+*:0 # 8(4;, 1235' -
!UI : 86 p ' p 1Activity/Fragment,
-$A(&B"% p 1 hop (&.* -$A(&.* B"% p )# p Paging RecyclerView + -
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') } } -
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 " -
UI : Shared Test 89 src/sharedTest src/androidTest src/test
src/sharedTest Instrumented Test Local Test -
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) -
!UI: 91 p "$#.' p
)! p 1 Activity/Fragment- p 1 hop *(/, p Shared Test%& UI p PageObject+! -
*( 93 Espresso1 ! Robolectric+ 2,87 p Robolectric
static3/DB#$% p IdlingResource1 6 & '4 p JetpackEspresso API0)5-. p 1 p RoomWorkManagerNavigationViewPaging5- p " -
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 -
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