$30 off During Our Annual Pro Sale. View Details »

AndroidX Test

Yuki Anzai
December 18, 2018

AndroidX Test

Yuki Anzai

December 18, 2018
Tweet

More Decks by Yuki Anzai

Other Decks in Technology

Transcript

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

    View Slide

  2. ͋Μ͍͟Ώ͖
    • blog : Y.A.M の雑記帳
    • y-anz-m.blogspot.com
    • twitter : @yanzm (やんざむ)
    • uPhyca Inc. (株式会社ウフィカ)
    • Google Developers Expert for Android

    View Slide

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

    View Slide

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

    View Slide

  5. 2種類のテスト
    • Local tests
    • test/
    • JVM上
    • Instrumented tests
    • androidTest/
    • デバイス・エミュレータ上
    https://developer.android.com/training/testing/unit-testing/

    View Slide

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

    View Slide

  7. AndroidX Test

    View Slide

  8. AndroidX Test
    • minSdkVersion が 14, targetSdkVersion が 28
    • androidx.core:core が追加
    • androidx.test.ext:truth が追加
    • androidx.test.ext.junit が追加

    View Slide

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

    View Slide

  10. What's new

    View Slide

  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"

    View Slide

  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 を⾃動切り替え

    View Slide

  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/

    View Slide

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

    View Slide

  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()

    View Slide

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

    View Slide

  17. ActivityScenario
    • テストのために Activity のライフサイクルを進めることができる
    • onResume() の後
    • onPause() の後かつ onStop() の前
    • onStop() の後かつ onDestroy() の前
    • onDestroy() の後
    • Robolectric の ActivityController や Android Testing Support
    Library の ActivityTestRule の置き換え
    "androidx.test:core:1.1.0"

    View Slide

  18. ActivityScenario
    • ライフサイクルの状態の指定には Android Architecture
    Components の Lifecycle.State を利⽤
    "androidx.test:core:1.1.0"

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }
    ライフサイクル状態の変更

    View Slide

  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"

    View Slide

  23. 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

    View Slide

  24. @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

    View Slide

  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

    View Slide

  26. FragmentScenario
    • テストのために Fragment のライフサイクルを進めること
    ができる
    • ライフサイクルの状態の指定には Android Architecture
    Components の Lifecycle.State を利⽤
    • DESTROYED に達したあとは Fragment を他の状態にでき
    ない
    • 再⽣成のテストには recreate() を使う
    "androidx.fragment:fragment-testing:1.1.0-alpha02"

    View Slide

  27. @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
    }
    }

    View Slide

  28. 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)
    }

    View Slide

  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"

    View Slide

  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 {

    }
    }

    View Slide

  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()
    }
    }

    View Slide

  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"

    View Slide

  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

    View Slide

  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")))

    View Slide

  35. Set up

    View Slide

  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"

    View Slide

  37. @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"

    View Slide

  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"

    View Slide

  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"

    View Slide

  40. まとめ
    • AndroidJUnit4 クラスのパッケージが変更
    • ApplicationProvider で Context を取得
    • core の ActivityScenario, fragment-testing の
    FragmentScenario でライフサイクル状態を変更してテスト
    • Assertion ライブラリ Truth の拡張が androidx.test.ext:truth
    で提供
    • Robolectric を使った Local tests と、on-device の
    Instrumented tests で同じテストが実⾏可能

    View Slide

  41. おまけ

    View Slide

  42. AndroidX Test
    最新 ver と stable ver

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  46. Monitor
    最新 :
    Stable : 2018/12/13 androidx.test:monitor:1.1.1

    View Slide

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

    View Slide

  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"

    View Slide

  49. おまけ 2

    View Slide

  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"

    View Slide