AndroidX Test

D2bcabeeb1ddff142fb8988b412cb4d3?s=47 Yuki Anzai
December 18, 2018

AndroidX Test

D2bcabeeb1ddff142fb8988b412cb4d3?s=128

Yuki Anzai

December 18, 2018
Tweet

Transcript

  1. AndroidX Test あんざいゆき(yanzm)

  2. ͋Μ͍͟Ώ͖ • blog : Y.A.M の雑記帳 • y-anz-m.blogspot.com • twitter

    : @yanzm (やんざむ) • uPhyca Inc. (株式会社ウフィカ) • Google Developers Expert for Android
  3. https://www.youtube.com/watch?v=4m2yYSTdvIg

  4. テスト書いてますか?

  5. 2種類のテスト • Local tests • test/ • JVM上 • Instrumented

    tests • androidTest/ • デバイス・エミュレータ上 https://developer.android.com/training/testing/unit-testing/
  6. iterative, test-driven development https://developer.android.com/training/testing/fundamentals

  7. AndroidX Test

  8. AndroidX Test • minSdkVersion が 14, targetSdkVersion が 28 •

    androidx.core:core が追加 • androidx.test.ext:truth が追加 • androidx.test.ext.junit が追加
  9. AndroidX Test • Core • JUnit (AndroidJUnitRunner, JUnit Rules and

    JUnit extension) • Assertions (Truth extension) • Monitor • AndroidTestOrchestrator • Espresso
  10. What's new

  11. 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"
  12. • 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 を⾃動切り替え
  13. 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/
  14. https://www.youtube.com/watch?v=4m2yYSTdvIg

  15. 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()
  16. 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"
  17. ActivityScenario • テストのために Activity のライフサイクルを進めることができる • onResume() の後 • onPause()

    の後かつ onStop() の前 • onStop() の後かつ onDestroy() の前 • onDestroy() の後 • Robolectric の ActivityController や Android Testing Support Library の ActivityTestRule の置き換え "androidx.test:core:1.1.0"
  18. ActivityScenario • ライフサイクルの状態の指定には Android Architecture Components の Lifecycle.State を利⽤ "androidx.test:core:1.1.0"

  19. ActivityScenario • Activity 起動後の状態は RESUMED "androidx.test:core:1.1.0"

  20. ActivityScenario • DESTROYED に達したあとは Activity を他の状態にできない • 再⽣成のテストには recreate() を使う

    "androidx.test:core:1.1.0"
  21. "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 } } ライフサイクル状態の変更
  22. @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"
  23. 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
  24. @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
  25. @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
  26. FragmentScenario • テストのために Fragment のライフサイクルを進めること ができる • ライフサイクルの状態の指定には Android Architecture

    Components の Lifecycle.State を利⽤ • DESTROYED に達したあとは Fragment を他の状態にでき ない • 再⽣成のテストには recreate() を使う "androidx.fragment:fragment-testing:1.1.0-alpha02"
  27. @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 } }
  28. 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) }
  29. 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"
  30. 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> { … } }
  31. 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() } }
  32. Truth • フレームワークのクラス(Intent や Notification など)向 けの 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"
  33. NotificationActionSubject assertThat(Notification.Action action) NotificationSubject assertThat(Notification notification) 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
  34. 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")))
  35. Set up

  36. @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"
  37. @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"
  38. @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"
  39. @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"
  40. まとめ • AndroidJUnit4 クラスのパッケージが変更 • ApplicationProvider で Context を取得 •

    core の ActivityScenario, fragment-testing の FragmentScenario でライフサイクル状態を変更してテスト • Assertion ライブラリ Truth の拡張が androidx.test.ext:truth で提供 • Robolectric を使った Local tests と、on-device の Instrumented tests で同じテストが実⾏可能
  41. おまけ

  42. AndroidX Test 最新 ver と stable ver

  43. 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
  44. 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
  45. 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
  46. Monitor 最新 : Stable : 2018/12/13 androidx.test:monitor:1.1.1

  47. AndroidTestOrchestrator 最新 : Stable : 2018/12/13 androidx.test:orchestrator:1.1.1

  48. 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"
  49. おまけ 2

  50. 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"