ニッチな用途で汎用性はない気がしますが、うまく行ってちょっと嬉しかったのでメモ。
問題
下図のように、RecyclerView ベースのアプリがありまして、内部状態によって ViewHolder にボーダーをつけたりつけなかったりしています。ボーダーの有る無しは、background に異なる shape を指定することで実現しています。(stroke を実装した shape ではボーダーが付き、stroke がなければ付きません。)
ところが、期待した ViewHolder 以外にもボーダーが描画されるというバグが発生しました。おそらく ViewHolder の再利用によるバグだと思います。すぐに直せばいいんですが、これは UI テストを書くところだろうと言うことで、初めてちゃんと Espresso テストをやってみました。
しかし、こう言う見た目を比べる Matcher て言うのはコアのライブラリではちゃんと用意されてなくて、サードパーティでも見つからなかったので自分で書いてみることにしました。(初心者なので見落としている可能性も大ですが)
itemView.background
ViewHolder の itemView.background の状態を調べるわけなんで、それが何クラスに属しているのかをまず調べます。デバッグして、ちまちまViewHolder のオブジェクトツリーを辿ります。
ありました!mBackground プロパティは GradientDrawable のインスタンスで、こいつには mStrokePaint っていうプロパティが有ります。このプロパティに実体があればボーダー有り、null だったらボーダー無しです。
Matcher
Matcher の実装は以下です。
fun backgroundHasBorder(): Matcher<View> {
return object : TypeSafeMatcher<View>(View::class.java!!) {
override fun describeTo(description: Description) {
description.appendText("to have a border")
}
override fun matchesSafely(foundView: View): Boolean {
val gradientDrawable = (foundView.background as? GradientDrawable)
if (gradientDrawable == null) {
print("Background is not a gradient")
return false
}
val field = (GradientDrawable::class.java.getDeclaredField("mStrokePaint"))
field.isAccessible = true
val stroke = field.get(gradientDrawable)
return stroke != null
}
}
}
GradientDrawable.mStrokePoint はプライベートプロパティで、これを公開しているメソッドがないので、リフレクションでアクセスしています。
使い方
ボーダーがある(はず)の時
onView(withId(R.id.SomeId))
.check(matches(backgroundHasBorder()))
ボーダーがない(はず)の時
onView(withId(R.id...))
.check(not(matches(backgroundHasBorder())))
NOTE : 今回の僕のアプリの場合、RecyclerView の ViewHolder について評価したいわけですが、RecyclerView の個々の ViewHolder アクセスには withId(...) Matcher が使えず、カスタムの Matcher を使用しています。1
感想
-
shapeで定義したbackgroundがShapeDrawableではなく、GradientDrawableになるっていうのは意外でした。きっと、rect や ellipse ベースではないもっと複雑なshapeを定義するとShapeDrawableになるのかな?- この辺はバージョンにおける実装の違いが怖いですが、一応 API level 28 (Pie) と API level 19 (Kitkat) でテストしたところ、期待通りに動きました。
- Java や Kotlin には reflection があるので、こういう
ViewのMatcherを書いたりするのは、調べさえすればワンパスは通せるという感じがあります。ただ、Proguard をかますと間違いなくハマりそう。
-
一般的に
RecyclerViewアクセスが思ったより大変でハマりました。他にも方法はあるのかもしれませんが RecyclerViewMatcher というのをコピーしてきて使いました。これを使う場合、View の Matcher にはwithId(...)でアクセスするのではなく、withRecyclerView(R.id....).atPosition(...)でアクセスします。 ↩