2020/12/02追記: Activityじゃなくてfragment-testingを使いましょうという感じの記事をかきました
追記ここまで
ViewGroup
のonLayout()
が呼ばれた後でいろいろやるクラスを作ることになったので、ユニットテストを書きながら実装を進めることにした。
onLayout()
を呼んでもらうにはViewGroup
をActivity
にアタッチ(setConentView
)しなければならない。
ActivityScenario.launch(MainActivity::class.java).onActivity { activity -> val sut = TargetViewGroup(activity) it.setContentView(sut) }
とりあえずテストする分には適当なActivity
にアタッチすればいいが、MainActivity
にいろんな実装が入ったりするとテストの準備が大変なことになるのでテスト用のActivity
を用意しておいた方がいい。テスト用のActivity
もAndroidManifest.xml
に登録しておかなければならないので、テスト用の共通モジュールを作ってmainソースの中に入れておくと便利になる。Theme込みのテストがしたければAppCompatActivity
を継承し、Styleを定義して適用しておく。これが必要なければ普通のActivityでよい。
あとは順番にテストを書いていけばいいのだが、Robolectric 4.3で入った@LooperMode
というやつを使うことで、例えば1フレームの中でaddView()
を複数回呼んだ時の挙動を本来のものにより近づけることができる。それ以前のRobolectricではaddView()
を呼ばれたらその都度(requestLayout
を経由して) onMeasure -> onLayoutが呼ばれていた。
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