Share
Embed
AndroidX Test
Android Dev Summit 2018報告会
https://connpass.com/event/109464/
Yuki Anzai
December 10, 2018
Transcript
50 slides
-
͋Μ͍͟Ώ͖ • blog : Y.A.M ͷࡶهா • y-anz-m.blogspot.com • twitter
: @yanzm ʢΜ͟Ήʣ • uPhyca Inc. (גࣜձࣾϑΟΧ) • Google Developers Expert for Android -
2छྨͷςετ • Local tests • test/ • JVM্ • Instrumented
tests • androidTest/ • σόΠεɾΤϛϡϨʔλ্ https://developer.android.com/training/testing/unit-testing/ -
AndroidX Test • minSdkVersion ͕ 14, targetSdkVersion ͕ 28 •
androidx.core:core ͕Ճ • androidx.test.ext:truth ͕Ճ • androidx.test.ext.junit ͕Ճ -
AndroidX Test • Core • JUnit (AndroidJUnitRunner, JUnit Rules and
JUnit extension) • Assertions (Truth extension) • Monitor • AndroidTestOrchestrator • Espresso -
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" -
• 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 ΛࣗಈΓସ͑ -
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/ -
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() -
ApplicationProvider • ςετରΞϓϦͷContext Λऔಘ͢Δ৽͍͠ํ๏ • androidx.test.core.app.ApplicationProvider Λ͏ • androidx.test.InstrumentationRegistry ͷ
getContext(), getTargetContext() deprecated • Robolectric ͷ RuntimeEnvironment.application deprecated val appContext = ApplicationProvider.getApplicationContext<Context>() assertEquals("net.yanzm.jetpacksamples", appContext.packageName) "androidx.test:core:1.1.0" -
ActivityScenario • ςετͷͨΊʹ Activity ͷϥΠϑαΠΫϧΛਐΊΔ͜ͱ͕Ͱ͖Δ • onResume() ͷޙ • onPause()
ͷޙ͔ͭ onStop() ͷલ • onStop() ͷޙ͔ͭ onDestroy() ͷલ • onDestroy() ͷޙ • Robolectric ͷ ActivityController Android Testing Support Library ͷ ActivityTestRule ͷஔ͖͑ "androidx.test:core:1.1.0" -
ActivityScenario • DESTROYED ʹୡͨ͋͠ͱ Activity Λଞͷঢ়ଶʹͰ͖ͳ͍ • ࠶ੜͷςετʹ recreate() Λ͏
"androidx.test:core:1.1.0" -
"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 } } ϥΠϑαΠΫϧঢ়ଶͷมߋ -
@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" -
import androidx.test.core.app.launchActivity val scenario = launchActivity<MainActivity>() "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 -
@RunWith(AndroidJUnit4::class) class MainActivityUnitTest { @Test fun test() { val scenario
= launchActivity<MainActivity>() 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 -
@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 -
FragmentScenario • ςετͷͨΊʹ Fragment ͷϥΠϑαΠΫϧΛਐΊΔ͜ͱ ͕Ͱ͖Δ • ϥΠϑαΠΫϧͷঢ়ଶͷࢦఆʹ Android Architecture
Components ͷ Lifecycle.State Λར༻ • DESTROYED ʹୡͨ͋͠ͱ Fragment Λଞͷঢ়ଶʹͰ ͖ͳ͍ • ࠶ੜͷςετʹ recreate() Λ͏ "androidx.fragment:fragment-testing:1.1.0-alpha02" -
@RunWith(AndroidJUnit4::class) class MainFragmentTest { @Test fun test() { val scenario
= FragmentScenario.launch(MainFragment::class.java) // or // val scenario = launchFragment<MainFragment>() 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 } } -
val args = bundleOf("name" to "yanzm") val scenario = FragmentScenario.launch(MainFragment::class.java,
args) // or // val scenario = launchFragment<MainFragment>(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<MainFragment>() scenario.onFragment { fragment -> assertThat(fragment.id).isEqualTo(android.R.id.content) } -
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" -
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<Person> { … } } -
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() } } -
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" -
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 -
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"))) -
@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" -
@RunWith(AndroidJUnit4::class) class LoginActivityUnitTest { @Test fun test() { val scenario
= launchActivity<LoginActivity>() 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" -
@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" -
@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" -
·ͱΊ • AndroidJUnit4 Ϋϥεͷύοέʔδ͕มߋ • ApplicationProvider Ͱ Context Λऔಘ •
core ͷ ActivityScenario, fragment-testing ͷ FragmentScenario ͰϥΠϑαΠΫϧঢ়ଶΛมߋͯ͠ςετ • Assertion ϥΠϒϥϦ Truth ͷ֦ு͕ androidx.test.ext:truth Ͱఏڙ • Robolectric Λͬͨ Local tests ͱɺon-device ͷ Instrumented tests Ͱಉ͡ςετ͕࣮ߦՄೳ -
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 -
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 -
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 -
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" -
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"