概要
Androidテスト全書なども発刊されたり、今回のDroidKaigi2019でもAndroidのテスト周りの発表が多かったように思います。
直近のプロジェクトでもテストのカバレッジ50%以上を目指してテストコードを書いているのでその中でのハマったところやどういった観点でテストコードを書いたかまとめておきます。
テストコードを書いているプロジェクトの構成
アーキテクチャ
AACを使ったMVVMの形を採用しています。
Activity/Fragment <=> ViewModel <=> Repository になっていてAPIを実行した結果をUI側の都合に合わせてViewModelで整形して、Activity/Fragment側にLiveDataを通じて流す形です。
依存解決にはDaggerを使います。
使っているUIライブラリ
- DataBinding
- Epoxy
使うテスト用ライブラリ
- org.robolectric:robolectric:4.1
- androidx.test:runner :1.1.0
- テストランナーにAndroidJUnit4を指定しておけば、実行時にInstrumental/Localテストを判別してAndroidJUnit4ClassRunner/RobolectricTestRunnerを切り替えてくれます
- io.mockk:mockk:1.9.1
- apiやrepositoryなどを必要に応じmock化する、実行回数のverifyなどに利用
- androidx.test.espresso:espresso-core:3.1.2-alpha02
- androidx対応後のライブラリはローカルテストでもEspressoを使ってUIの操作ができるようになった
その他
コードのカバレッジはjacocoを使っています。
各テスト対象についてどのようにテストするか
テストしやすい対象
UtilityClass
- UtilityクラスはAndroid Platformのクラスが比較的出てこないのでJunitさえわかってれば簡単にかける
- 基本は境界値だったりで正しさを確認する。
fun max(x: Int, y: Int): Int {
return if (x > y) x else y
}
@Test
fun testMax() {
assertThat(max(0, 1)).isEqualsTo(1)
assertThat(max(0, 0)).isEqualsTo(0)
assertThat(max(1, 0)).isEqualsTo(1)
}
BindingAdapters
- robolectricでshadowされるViewを引数にテスト対象のメソッドに渡して、assertionでViewが持つ値を確認することで正しさを確認する。
fun bindText(view: TextView, value: String) {
view.text = value
}
var mockView = TextView(ApplicationProvider.getApplicationContext<Context>())
@Test
fun test() {
bindText(mockView, "hello")
assertThat(mockView.text).isEqualTo("hello")
}
EpoxyController
- buildModel後のmodelsをinterceptするinterfaceが用意されているので、下記の様な拡張関数を用意することで、buildModel後のmodelの配列を取得できる。
取得できたmodelの配列の順序やclassをassertionすることで、RecyclerViewの表示の正しさを確認する。
// block: requestBuildModelsを実行する前にControllerに値を渡す処理などを実行する関数
inline fun <T : EpoxyController> T.interceptModels(block: T.() -> Unit): List<List<EpoxyModel<*>>> {
val models = mutableListOf<List<EpoxyModel<*>>>()
val interceptor =
mockk<EpoxyController.Interceptor> { every { intercept(capture(models)) } just runs }
addInterceptor(interceptor)
block()
removeInterceptor(interceptor)
return models
}
@Test
fun test() {
val controller = HogeController()
val models = controller.interceptModels {
setValue("hoge")
requestModelBuild()
}
assertThat(models).hasSize(1)
assertThat(models[0][0]).isInstanceOf(TitleModel::class)
assertThat(models[0][0].title).isEqualTo("hoge")
}
EpoxyModel
- テスト対象になるModelのBindingClassやViewHolderのinstanceを作る/mockする。
- Model自体はAbstractClassなので、テスト用に
class TestModel: TargetModel()
みたいな形でinstanceを作れるようにする. bind
のメソッドにBindingClassやViewHolderのinstanceを渡して、その後assertionすることでModelの振る舞いの正しさを確認する。
abstract TitleModel: DataBindingModel<TitleDataBinding>() {
@EpoxyAttribute
lateinit var title: String
override fun bind(binding: TitleDataBinding) {
binding.title.text = title
}
}
@Test
fun test() {
val model = TitleModel_()
val layoutInflater = LayoutInflater.from(ApplicationProvider.getApplicationContext())
val binding = DataBindingUtil.inflate(layoutInflater, R.layout.title, null, false)
model.title = "hoge"
model.bind(binding)
assertThat(binding.title.text).isEqualTo("hoge")
}
ViewModel
- 依存するrepositoryをmockする
- coroutine使っていて非同期処理の問題が出てきたので、Main/IOのスレッドを置き換えられるように、ContextProviderというInterfaceを用意してテスト時にはMain/IOを揃えられるようにした。
- https://github.com/Kotlin/kotlinx.coroutines/tree/master/core/kotlinx-coroutines-test ver1.1.0ではMainDispatcherを置き換えられるようになっているのだが、IOと合わせないと非同期に実行されてしまってテストが意図した動作しないので使わずに置き換える形に渋々。
- ViewModelに生やしているメソッドを実行して、repositoryなどから取得したデータがLiveDataに流れてくるかで正しさを確認する。
テストするのに準備ややり方が大変な対象
Daggerを使っていることもあり、それらの依存関係が解消できないとそもそも起動しないのでテストすらできない。
RobolectricはApplicationは指定できるのでTestAppを作ってテスト用に作ったComponent/Moduleを用いて解決するようにする。
Activity
- SingleActivity/MultiFragmentの構成なのでActivityは書いてない.
Fragment
- NavigationComponentを使っているとFragmentScenario使うと遷移とかの確認ができなくなる(NavHostFragmentがいなくなるので)。app側が持つlayoutファイルを使うTestActivityを用意して、そのActivityに遷移させるメソッドを用意して、ActivityScenarioでTestActivityを起動、対象のFragmentに遷移としてからテストをするようにした。
- ktxにlaunchActivityというメソッドがあるがこれを使おうとするとjvmTargetを1.8にあげないと行けないが上げるとhashCodeの実装がAPILevel21あたりから変わっていることでhashCodeのメソッドが見つからずcrashする。
- SpinnerがEspressoを使うと
onData
でデータを指定してリストを選択するようなコードが実機上は行えるがローカルテストではPopupListViewが別レイヤーにて表示されている関係で選択ができない。- ViewActionを実装して直接setSelectionするようなコードが必要になります。
androidx.test.espresso:espresso-contrib
はRecyclerViewやViewPagerの操作のユーティリティーが集まってるのでViewのテストするときは入れたほうが楽になります。
今後挑戦すること
- DroidKaigiの発表で見たようにFragmentのテストをAndroidTest/test両方から見れるように別に切り出すことで、共通のコードで行えるようにする。