Slide 1

Slide 1 text

AndroidX Test ͋Μ͍͟Ώ͖ʢyanzmʣ Android Dev Summit 2018ใࠂձ

Slide 2

Slide 2 text

͋Μ͍͟Ώ͖ • blog : Y.A.M ͷࡶهா • y-anz-m.blogspot.com • twitter : @yanzm ʢ΍Μ͟Ήʣ • uPhyca Inc. (גࣜձࣾ΢ϑΟΧ) • Google Developers Expert for Android

Slide 3

Slide 3 text

https://www.youtube.com/watch?v=4m2yYSTdvIg

Slide 4

Slide 4 text

ςετॻ͍ͯ·͔͢ʁ

Slide 5

Slide 5 text

2छྨͷςετ • Local tests • test/ • JVM্ • Instrumented tests • androidTest/ • σόΠεɾΤϛϡϨʔλ্ https://developer.android.com/training/testing/unit-testing/

Slide 6

Slide 6 text

iterative, test-driven development https://developer.android.com/training/testing/fundamentals

Slide 7

Slide 7 text

AndroidX Test

Slide 8

Slide 8 text

AndroidX Test • minSdkVersion ͕ 14, targetSdkVersion ͕ 28 • androidx.core:core ͕௥Ճ • androidx.test.ext:truth ͕௥Ճ • androidx.test.ext.junit ͕௥Ճ

Slide 9

Slide 9 text

AndroidX Test • Core • JUnit (AndroidJUnitRunner, JUnit Rules and JUnit extension) • Assertions (Truth extension) • Monitor • AndroidTestOrchestrator • Espresso

Slide 10

Slide 10 text

What's new

Slide 11

Slide 11 text

AndroidJUnit4 • androidx.test.runner.AndroidJunit4 ͕ deprecated ʹͳ Γɺ୅ΘΓʹ androidx.test.ext.junit.runners.AndroidJUnit4 Λ࢖͏ import androidx.test.ext.junit.runners.AndroidJUnit4 @RunWith(AndroidJUnit4::class) class ExampleTest { "androidx.test.ext:junit:1.1.0"

Slide 12

Slide 12 text

• Local tests → RobolectricTestRunner • Instrumented tests → AndroidJUnit4ClassRunner 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; } AndroidJUnit4 ͷதͰ Runner Λࣗಈ੾Γସ͑

Slide 13

Slide 13 text

test/ @RunWith(AndroidJUnit4::class) class ExampleUnitTest { @Test fun test() { assertThat(TextUtils.isDigitsOnly("123")).isTrue() } } @RunWith(AndroidJUnit4::class) class ExampleInstrumentedUnitTest { @Test fun test() { assertThat(TextUtils.isDigitsOnly("123")).isTrue() } } androidTest/

Slide 14

Slide 14 text

https://www.youtube.com/watch?v=4m2yYSTdvIg

Slide 15

Slide 15 text

ApplicationProvider • ςετର৅ΞϓϦͷContext Λऔಘ͢Δ৽͍͠ํ๏ • ࠓ·Ͱ Robolectric Ͱ͸ • ࠓ·Ͱ Instrumented tests Ͱ͸ • ςετίʔυͷ Context • ςετର৅ͷΞϓϦͷ Context "androidx.test:core:1.1.0" val context : Context = RuntimeEnvironment.application val context = InstrumentationRegistry.getContext() val context = InstrumentationRegistry.getTargetContext()

Slide 16

Slide 16 text

ApplicationProvider • ςετର৅ΞϓϦͷContext Λऔಘ͢Δ৽͍͠ํ๏ • androidx.test.core.app.ApplicationProvider Λ࢖͏ • androidx.test.InstrumentationRegistry ͷ getContext(), getTargetContext() ͸ deprecated • Robolectric ͷ RuntimeEnvironment.application ͸ deprecated val appContext = ApplicationProvider.getApplicationContext() assertEquals("net.yanzm.jetpacksamples", appContext.packageName) "androidx.test:core:1.1.0"

Slide 17

Slide 17 text

ActivityScenario • ςετͷͨΊʹ Activity ͷϥΠϑαΠΫϧΛਐΊΔ͜ͱ͕Ͱ͖Δ • onResume() ͷޙ • onPause() ͷޙ͔ͭ onStop() ͷલ • onStop() ͷޙ͔ͭ onDestroy() ͷલ • onDestroy() ͷޙ • Robolectric ͷ ActivityController ΍ Android Testing Support Library ͷ ActivityTestRule ͷஔ͖׵͑ "androidx.test:core:1.1.0"

Slide 18

Slide 18 text

ActivityScenario • ϥΠϑαΠΫϧͷঢ়ଶͷࢦఆʹ͸ Android Architecture Components ͷ Lifecycle.State Λར༻ "androidx.test:core:1.1.0"

Slide 19

Slide 19 text

ActivityScenario • Activity ىಈޙͷঢ়ଶ͸ RESUMED "androidx.test:core:1.1.0"

Slide 20

Slide 20 text

ActivityScenario • DESTROYED ʹୡͨ͋͠ͱ͸ Activity Λଞͷঢ়ଶʹͰ͖ͳ͍ • ࠶ੜ੒ͷςετʹ͸ recreate() Λ࢖͏ "androidx.test:core:1.1.0"

Slide 21

Slide 21 text

"androidx.test:core:1.1.0" @RunWith(AndroidJUnit4::class) class MainActivityTest { @Test fun test() { val scenario = ActivityScenario.launch(MainActivity::class.java) scenario.onActivity { activity -> // activity ͸ resumed : onResume() ͷޙ assertThat(activity.getSomething()).isEqualTo("something") } scenario.moveToState(Lifecycle.State.STARTED) scenario.onActivity { activity -> // activity ͸ paused : onPause() ͷޙɺ onStop() ͷલ } scenario.moveToState(Lifecycle.State.CREATED) scenario.onActivity { activity -> // activity ͸ stopped : onStop() ͷޙɺ onDestroy() ͷલ } scenario.moveToState(Lifecycle.State.DESTROYED) // activity ͸ destroyed and finished } } ϥΠϑαΠΫϧঢ়ଶͷมߋ

Slide 22

Slide 22 text

@RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun test() { // GIVEN val username = "yanzm" val scenario = ActivityScenario.launch(LoginActivity::class.java) // WHEN onView(withId(R.id.usernameEditText)).perform(typeText(username)) scenario.recreate() // Activity Λ࠶ੜ੒ // THEN onView(withId(R.id.usernameEditText)).check(matches(withText(username))) } } ࠶ੜ੒ͷςετ "androidx.test:core:1.1.0"

Slide 23

Slide 23 text

import androidx.test.core.app.launchActivity val scenario = launchActivity() "androidx.test:core-ktx:1.1.0" tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { jvmTarget = '1.8' } } cannot inline bytecode build with jvm target 1.7 into bytecode that is being build with jvm target 1.6 ͱ͍͏Τϥʔ͕ग़ͨ৔߹ɺҎԼͷઃఆ͕ඞཁ core-ktx

Slide 24

Slide 24 text

@RunWith(AndroidJUnit4::class) class MainActivityUnitTest { @Test fun test() { val scenario = launchActivity() scenario.onActivity { activity -> // activity ͸ resumed : onResume() ͷޙ Truth.assertThat(activity.getSomething()).isEqualTo("something") } scenario.moveToState(Lifecycle.State.STARTED) scenario.onActivity { activity -> // activity ͸ paused : onPause() ͷޙɺ onStop() ͷલ } scenario.moveToState(Lifecycle.State.CREATED) scenario.onActivity { activity -> // activity ͸ stopped : onStop() ͷޙɺ onDestroy() ͷલ } scenario.moveToState(Lifecycle.State.DESTROYED) // activity ͸ destroyed and finished } } JUnit (Robolectric) testImplementation "junit:junit:4.12" testImplementation "androidx.test:core-ktx:1.1.0" testImplementation "com.google.truth:truth:0.42" testImplementation "androidx.test.ext:truth:1.1.0" testImplementation "androidx.test.ext:junit-ktx:1.1.0" testImplementation "org.robolectric:robolectric:4.0.2" AndroidJUnit4 Ͱ OK

Slide 25

Slide 25 text

@RunWith(AndroidJUnit4::class) class LoginActivityUnitTest { @Test fun test() { // GIVEN val username = "yanzm" val scenario = ActivityScenario.launch(LoginActivity::class.java) // WHEN onView(withId(R.id.usernameEditText)).perform(typeText(username)) scenario.recreate() // Activity Λ࠶ੜ੒ // THEN onView(withId(R.id.usernameEditText)).check(matches(withText("yanzm"))) } } JUnit (Robolectric) testImplementation "junit:junit:4.12" testImplementation "androidx.test:core-ktx:1.1.0" testImplementation "com.google.truth:truth:0.42" testImplementation "androidx.test.ext:truth:1.1.0" testImplementation "androidx.test.ext:junit-ktx:1.1.0" testImplementation "org.robolectric:robolectric:4.0.2" testImplementation "androidx.test.espresso:espresso-core:3.1.1" AndroidJUnit4 Ͱ OK

Slide 26

Slide 26 text

FragmentScenario • ςετͷͨΊʹ Fragment ͷϥΠϑαΠΫϧΛਐΊΔ͜ͱ ͕Ͱ͖Δ • ϥΠϑαΠΫϧͷঢ়ଶͷࢦఆʹ͸ Android Architecture Components ͷ Lifecycle.State Λར༻ • DESTROYED ʹୡͨ͋͠ͱ͸ Fragment Λଞͷঢ়ଶʹͰ ͖ͳ͍ • ࠶ੜ੒ͷςετʹ͸ recreate() Λ࢖͏ "androidx.fragment:fragment-testing:1.1.0-alpha02"

Slide 27

Slide 27 text

@RunWith(AndroidJUnit4::class) class MainFragmentTest { @Test fun test() { val scenario = FragmentScenario.launch(MainFragment::class.java) // or // val scenario = launchFragment() scenario.onFragment { fragment -> // fragment ͸ resumed : onResume() ͷޙ assertThat(fragment.getSomething()).isEqualTo("something") } scenario.moveToState(Lifecycle.State.STARTED) scenario.onFragment { fragment -> // fragment ͸ paused : onPause() ͷޙɺ onStop() ͷલ } scenario.moveToState(Lifecycle.State.CREATED) scenario.onFragment { fragment -> // fragment ͸ stopped : onStop() ͷޙɺ onDestroy() ͷલ } scenario.moveToState(Lifecycle.State.DESTROYED) // fragment ͸ destroyed and finished } }

Slide 28

Slide 28 text

val args = bundleOf("name" to "yanzm") val scenario = FragmentScenario.launch(MainFragment::class.java, args) // or // val scenario = launchFragment(args) scenario.onFragment { fragment -> BundleSubject.assertThat(fragment.arguments).string("name").isEqualTo("yanzm") } Launch with Bundle Launch with Container val scenario = FragmentScenario.launchInContainer(MainFragment::class.java) // or // val scenario = launchFragmentInContainer() scenario.onFragment { fragment -> assertThat(fragment.id).isEqualTo(android.R.id.content) }

Slide 29

Slide 29 text

Builder • ApplicationInfoBuilder, PackageInfoBuilder, MotionEventBuilder, PointerCoordsBuilder, PointerPropertiesBuilder val motionEvent = MotionEventBuilder.newBuilder() .setAction(MotionEvent.ACTION_DOWN) .setPointer(100f, 100f) .build() val applicationInfo = ApplicationInfoBuilder.newBuilder() .setName("Sample") .setPackageName("net.yanzm.sample") .build() val packageInfo = PackageInfoBuilder.newBuilder() .setPackageName("net.yanzm.sample") .setApplicationInfo(applicationInfo) .build() "androidx.test:core:1.1.0"

Slide 30

Slide 30 text

Parcelables • Parcelable ͷςετͷϢʔςΟϦςΟ val person1 = Person("Android", 10) val person2 = Parcelables.forceParcel(person1, Person.CREATOR) assertThat(person1).isEqualTo(person2) "androidx.test:core:1.1.0" data class Person(val name: String, val age: Int) : Parcelable { … companion object CREATOR : Parcelable.Creator { … } }

Slide 31

Slide 31 text

Truth • Google ͕։ൃ͍ͯ͠Δ Java ͷ Assertion ϥΠϒϥϦ http://google.github.io/truth/ "com.google.truth:truth:0.42" "androidx.test.ext:truth:1.1.0" import com.google.common.truth.Truth.assertThat import org.junit.Test class ExampleUnitTest { @Test fun test() { assertThat("123".isNotEmpty()).isTrue() assertThat("123").isEqualTo("123") assertThat("123").isNotEmpty() assertThat("123").contains("2") assertThat("123").startsWith("1") assertThat("123").endsWith("3") assertThat("123").hasLength(3) assertThat("123").isNotNull() } }

Slide 32

Slide 32 text

Truth • ϑϨʔϜϫʔΫͷΫϥεʢIntent ΍ Noti fi cation ͳͲʣ޲ ͚ͷ Assertions ͕ android.test.ext:truth ʹೖ͍ͬͯΔ import androidx.test.ext.truth.content.IntentSubject.assertThat @RunWith(AndroidJUnit4::class) class ExampleUnitTest { @Test fun test() { val intent = Intent( Intent.ACTION_VIEW, Uri.parse("https://developer.android.com") ) assertThat(intent).hasAction(Intent.ACTION_VIEW) assertThat(intent).hasData(Uri.parse("https://developer.android.com")) } } "com.google.truth:truth:0.42" "androidx.test.ext:truth:1.1.0"

Slide 33

Slide 33 text

Noti fi cationActionSubject assertThat(Noti fi cation.Action action) Noti fi cationSubject assertThat(Noti fi cation noti fi cation) PendingIntentSubject assertThat(PendingIntent intent) IntentSubject assertThat(Intent intent) BundleSubject assertThat(Bundle bundle) ParcelableSubject assertThat(Parcelable parcelable) MotionEventSubject assertThat(MotionEvent event) PointerCoordsSubject assertThat(MotionEvent.PointerCoords other) PointerPropertiesSubject assertThat(MotionEvent.PointerProperties other) Subject

Slide 34

Slide 34 text

IntentCorrespondences val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.com")) // action ͚ͩΛൺֱ assertThat(listOf(intent)) .comparingElementsUsing(IntentCorrespondences.action()) .contains(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.google.co.jp"))) // data ͚ͩΛൺֱ assertThat(listOf(intent)) .comparingElementsUsing(IntentCorrespondences.data()) .contains(Intent(Intent.ACTION_MAIN, Uri.parse("https://developer.android.com"))) // action ͱ data Λൺֱ assertThat(listOf(intent)) .comparingElementsUsing( IntentCorrespondences.all( IntentCorrespondences.action(), IntentCorrespondences.data() ) ) .contains(Intent(Intent.ACTION_VIEW, Uri.parse("https://developer.android.com")))

Slide 35

Slide 35 text

Set up

Slide 36

Slide 36 text

@RunWith(AndroidJUnit4::class) class ExampleUnitTest { @Test fun test() { assertThat(TextUtils.isDigitsOnly("123")).isTrue() } } Local unit tests testImplementation "junit:junit:4.12" testImplementation "androidx.test:core-ktx:1.1.0" // or "androidx.test:core:1.1.0" // AndroidJUnit4 ʹඞཁ testImplementation "androidx.test.ext:junit-ktx:1.1.0" // or "androidx.test.ext:junit:1.1.0" // AndroidJUnit4 ܦ༝Ͱͷ robolectric ࣮ߦʹඞཁ testImplementation "org.robolectric:robolectric:4.0.2" // Truth ࢖͏ͳΒඞཁ testImplementation "com.google.truth:truth:0.42" testImplementation "androidx.test.ext:truth:1.1.0"

Slide 37

Slide 37 text

@RunWith(AndroidJUnit4::class) class LoginActivityUnitTest { @Test fun test() { val scenario = launchActivity() onView(withId(R.id.usernameEditText)).perform(typeText("yanzm")) scenario.recreate() // Activity Λ࠶ੜ੒ onView(withId(R.id.usernameEditText)).check(matches(withText("yanzm"))) } } Local Espresso tests testImplementation "junit:junit:4.12" testImplementation "androidx.test:core-ktx:1.1.0" // or "androidx.test:core:1.1.0" // AndroidJUnit4 ʹඞཁ testImplementation "androidx.test.ext:junit-ktx:1.1.0" // or "androidx.test.ext:junit:1.1.0" // AndroidJUnit4 ܦ༝Ͱͷ robolectric ࣮ߦʹඞཁ testImplementation "org.robolectric:robolectric:4.0.2" testImplementation "androidx.test.espresso:espresso-core:3.1.1"

Slide 38

Slide 38 text

@RunWith(AndroidJUnit4::class) class ExampleInstrumentedUnitTest { @Test fun test() { assertThat(TextUtils.isDigitsOnly("123")).isTrue() } } Instrumented unit tests androidTestImplementation "androidx.test:runner:1.1.1" androidTestImplementation "androidx.test:rules:1.1.1" // AndroidJUnit4 ʹඞཁ androidTestImplementation "androidx.test.ext:junit-ktx:1.1.0" // or "androidx.test.ext:junit:1.1.0" // Truth ࢖͏ͳΒඞཁ androidTestImplementation "com.google.truth:truth:0.42" androidTestImplementation "androidx.test.ext:truth:1.1.0"

Slide 39

Slide 39 text

@RunWith(AndroidJUnit4::class) class LoginActivityTest { @Test fun test() { val scenario = ActivityScenario.launch(LoginActivity::class.java) onView(withId(R.id.usernameEditText)).perform(typeText("yanzm")) scenario.recreate() // Activity Λ࠶ੜ੒ onView(withId(R.id.usernameEditText)).check(matches(withText("yanzm"))) } } Instrumented Espresso tests androidTestImplementation "androidx.test:runner:1.1.1" androidTestImplementation "androidx.test:rules:1.1.1" // AndroidJUnit4 ʹඞཁ androidTestImplementation "androidx.test.ext:junit-ktx:1.1.0" // or "androidx.test.ext:junit:1.1.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"

Slide 40

Slide 40 text

·ͱΊ • AndroidJUnit4 Ϋϥεͷύοέʔδ͕มߋ • ApplicationProvider Ͱ Context Λऔಘ • core ͷ ActivityScenario, fragment-testing ͷ FragmentScenario ͰϥΠϑαΠΫϧঢ়ଶΛมߋͯ͠ςετ • Assertion ϥΠϒϥϦ Truth ͷ֦ு͕ androidx.test.ext:truth Ͱఏڙ • Robolectric Λ࢖ͬͨ Local tests ͱɺon-device ͷ Instrumented tests Ͱಉ͡ςετ͕࣮ߦՄೳ

Slide 41

Slide 41 text

͓·͚

Slide 42

Slide 42 text

AndroidX Test ࠷৽ ver ͱ stable ver

Slide 43

Slide 43 text

Core ࠷৽ : Stable : 2018/12/13 androidx.test:core:1.1.0 1.1.0-beta01 - core-ktx artifact ͕௥Ճ - Custom intents Ͱ Activity Λىಈ͢ΔͨΊͷ ৽͍͠ ActivityScenario API - Activity ͷ݁ՌΛड͚औΔͨΊͷ৽͍͠ ActivityScenario API - ActivityScenario ͕ closeable ʹͳͬͨ androidx.test:core-ktx:1.1.0

Slide 44

Slide 44 text

AndroidJUnitRunner and JUnit Rules ࠷৽ : Stable : 2018/12/13 androidx.test:runner:1.1.1 1.1.1 Rules - ActivityTestRule ͕ deprecated ʹͳΓɺ୅ΘΓʹ ActivityScenarioRule Λ࢖͏ androidx.test:rules:1.1.1

Slide 45

Slide 45 text

Assertions ࠷৽ : Stable : 2018/12/13 androidx.test.ext:truth:1.1.0 1.1.0 Truth - BundleSubject ʹ bool(), parcelable(), parcelableAsType() ͕௥Ճ JUnit - junit-ktx artifact ͕௥Ճ androidx.test.ext:junit:1.1.0 androidx.test.ext:junit-ktx:1.1.0

Slide 46

Slide 46 text

Monitor ࠷৽ : Stable : 2018/12/13 androidx.test:monitor:1.1.1

Slide 47

Slide 47 text

AndroidTestOrchestrator ࠷৽ : Stable : 2018/12/13 androidx.test:orchestrator:1.1.1

Slide 48

Slide 48 text

Espresso ࠷৽ : Stable : 2018/12/13 "androidx.test.espresso:espresso-core:3.1.1" "androidx.test.espresso:espresso-contrib:3.1.1" "androidx.test.espresso:espresso-intents:3.1.1" "androidx.test.espresso:espresso-accessibility:3.1.1" "androidx.test.espresso:espresso-web:3.1.1" "androidx.test.espresso.idling:idling-concurrent:3.1.1" "androidx.test.espresso:espresso-idling-resource:3.1.1"

Slide 49

Slide 49 text

͓·͚ 2

Slide 50

Slide 50 text

MockitoJUnitRunner import android.content.Context import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner @RunWith(MockitoJUnitRunner::class) class ExampleUnitTest5 { @Mock private lateinit var mockContext: Context @Test fun test() { `when`(mockContext.getString(R.string.app_name)) .thenReturn("JetpackSamples") assertThat(mockContext.getString(R.string.app_name)) .isEqualTo("JetpackSamples") } } "org.mockito:mockito-core:2.19.0"