fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

自作のViewGroupをRobolectricでテストしつつ開発する

2020/12/02追記: Activityじゃなくてfragment-testingを使いましょうという感じの記事をかきました

akihito104.hatenablog.com

追記ここまで


ViewGrouponLayout()が呼ばれた後でいろいろやるクラスを作ることになったので、ユニットテストを書きながら実装を進めることにした。

onLayout()を呼んでもらうにはViewGroupActivityにアタッチ(setConentView)しなければならない。

ActivityScenario.launch(MainActivity::class.java).onActivity { activity ->
  val sut = TargetViewGroup(activity)
  it.setContentView(sut)
}

とりあえずテストする分には適当なActivityにアタッチすればいいが、MainActivityにいろんな実装が入ったりするとテストの準備が大変なことになるのでテスト用のActivityを用意しておいた方がいい。テスト用のActivityAndroidManifest.xmlに登録しておかなければならないので、テスト用の共通モジュールを作ってmainソースの中に入れておくと便利になる。Theme込みのテストがしたければAppCompatActivityを継承し、Styleを定義して適用しておく。これが必要なければ普通のActivityでよい。

あとは順番にテストを書いていけばいいのだが、Robolectric 4.3で入った@LooperModeというやつを使うことで、例えば1フレームの中でaddView()を複数回呼んだ時の挙動を本来のものにより近づけることができる。それ以前のRobolectricではaddView()を呼ばれたらその都度(requestLayoutを経由して) onMeasure -> onLayoutが呼ばれていた。

robolectric.org

LooperModeは引数を一つとるアノテーションで、テストクラスやテストメソッドにつける。Mode.PAUSEDを指定するとLooperの挙動が本来の動きに近くなる。例えば次のような感じ。

    @Test
    @LooperMode(LooperMode.Mode.PAUSED)
    fun test() {
        ActivityScenario.launch(ContainerActivity::class.java)
            .onActivity {
                val sut = TargetView(it)
                val adapter = mockk<TargetView.Adapter>().apply {
                    every { onMeasure() } just runs
                    every { onLayout() } just runs
                }
                sut.adapter = adapter
                it.setContentView(sut)
                shadowOf(getMainLooper()).idle()

                sut.addView(TextView(it))
                sut.addView(TextView(it))
                shadowOf(getMainLooper()).idle()

                assertThat(sut.childCount).isEqualTo(2)
                verify(exactly = 2) { adapter.onMeasure() }
                verify(exactly = 2) { adapter.onLayout() }
            }
    }

ここでのポイントは shadowOf(getMainLooper()).idle() で、このテストケースではこれを呼ぶごとにViewの更新ライフサイクルが進むことになる。LooperModeアノテーションを外すか、渡すモードをLEGACYに変えると、下2つのverifyブロックはexactry = 3 としてやることでテストが通るようになる。

ちなみにこのモードでテストが失敗すると、java.lang.Exception: Main looper has queued unexecuted runnables. This might be the cause of the test failure. You might need a shadowOf(getMainLooper()).idle() callのようなメッセージがでることがある。Activityがいる状態だとどうしても出てしまうので、本当にidle()を呼んで解決する状況かどうか見極めた方がいい。

https://gist.github.com/akihito104/d9076720e14dad439461ab8c03a6279d