ニッチな用途で汎用性はない気がしますが、うまく行ってちょっと嬉しかったのでメモ。
問題
下図のように、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(...)
でアクセスします。 ↩