ViewBinding を使うと View のアクセスを簡略化 & @NonNull
を標準として堅牢にロジックを作れるようになります。
ここでこの ViewBinding を用いて次のような View の操作をメソッドとして定義したクラスを作ったとして、そのクラスにあるメソッドをユニットテストすることを考えてみます。
例えば、次のような TextView がおいてあるレイアウトを定義してみたとします。
<ConstraintLayout
xmlnsandroid="http://schemas.android.com/apk/res/android"
xmlnsapp="http://schemas.android.com/apk/res-auto"
androidlayout_width="match_parent"
androidlayout_height="match_parent"
>
<TextView
androidid="@+id/sample_text"
androidlayout_width="0dp"
androidlayout_height="0dp"
applayout_constraintTop_toTopOf="parent"
applayout_constraintBottom_toBottomOf="parent"
applayout_constraintStart_toStartOf="parent"
applayout_constraintEnd_toEndOf="parent"
/>
</ConstraintLayout>
ViewBinding では上記の XML から FragmentSampleBinding
のような名前の ViewBinding クラスが生成されます。これをつかって View の操作を司るクラスを次のように定義してみます(特に MVP パターンを推奨するつもりはなく、ただ View の操作を司るクラスの命名として Presenter を用いています)。
class SamplePresenter(
private val binding: FragmentSampleBinding
) {
fun setSampleText(text: String) {
binding.sampleText.text = text
}
}
シンプルですね。findViewById
の必要がなくなり、かつ Null Safe に View へアクセスできます。
ここで setSampleText
関数に渡した文字列が sample_text
を ID とした TextView
のテキストとして設定されることをユニットテストしてみます。
ユニットテストでは Robolectric を使わず、モックフレームワークとして mockk を使う場面を想定しています。
ユニットテストの準備として FragmentSampleBinding
をうまくモックするとテストの見通しがよくなりますが、
ViewBinding で自動生成されるクラスの構成の問題で素直にモックが生成できません。
次にその自動生成コードを示しますが、モックの生成で問題となるのは各 View のフィールドです。
mockk ではモック対象のクラスが持っているフィールドを mock できず、かつ ViewBinding が持っている View のフィールドは final なのであとから代入することもできません。
(${module}/build/generated/data_binding_base_class_source_out/ 配下に生成されます)
public final class FragmentHomeBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final TextView sampleText;
}
これを回避するには、ViewBinding クラスが持っている bind
メソッドを経由して各フィールドに mock を差し込みます。
bind
メソッドは次のような実装になっています。
public final class FragmentHomeBinding implements ViewBinding {
@NonNull
public static FragmentHomeBinding bind(@NonNull View rootView) {
int id;
missingId: {
id = R.id.sample_text;
TextView sampleText = rootView.findViewById(id);
if (sampleText == null) {
break missingId;
}
return new FragmentSampleBinding((ConstraintLayout) rootView, sampleText);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
この定義をもとに mock を差し込む方法を次に示します。
bind
メソッドに渡す rootView は ConstraintLayout に明示的にキャストされるため、mock するときも ConstraintLayout としてモックを作ります。また基本的に XML 内の View はすべて rootView から探索するため、
XML 上の親子関係によらずレイアウト内の View をすべて rootView から返すようにモックします。
class SamplePresenterTest : BehaviorSpec() {
init {
Given("SamplePresenter") {
val sampleText: TextView = mockk()
val rootView: ConstraintLayout = mockk {
every { findViewById<TextView>(eq(R.id.sample_text)) } returns sampleText
}
val binding = FragmentSampleBinding = FragmentSampleBinding.bind(rootView)
val presenter = SamplePresenter()
When("Set text to sample_text view") {
presenter.setSampleText("foo")
Then("`foo` is set") {
verify { sampleText.text = eq("foo") }
}
}
}
}
}
レイアウト内にある要素が増えるとその分だけ View のモックが手間になりますが、モックを作るファクトリメソッドを定義しておき
必要に応じてテストケース固有のモックを差し込めるようにしておくと潰しの効く構成になります。