Android
Kotlin
テスト
Espresso
android開発

Espresso テストで background に border があるかどうかを調べる Matcher

ニッチな用途で汎用性はない気がしますが、うまく行ってちょっと嬉しかったのでメモ。


問題

下図のように、RecyclerView ベースのアプリがありまして、内部状態によって ViewHolder にボーダーをつけたりつけなかったりしています。ボーダーの有る無しは、background に異なる shape を指定することで実現しています。(stroke を実装した shape ではボーダーが付き、stroke がなければ付きません。)

Japanese.png

ところが、期待した ViewHolder 以外にもボーダーが描画されるというバグが発生しました。おそらく ViewHolder の再利用によるバグだと思います。すぐに直せばいいんですが、これは UI テストを書くところだろうと言うことで、初めてちゃんと Espresso テストをやってみました。

しかし、こう言う見た目を比べる Matcher て言うのはコアのライブラリではちゃんと用意されてなくて、サードパーティでも見つからなかったので自分で書いてみることにしました。(初心者なので見落としている可能性も大ですが)


itemView.background

ViewHolderitemView.background の状態を調べるわけなんで、それが何クラスに属しているのかをまず調べます。デバッグして、ちまちまViewHolder のオブジェクトツリーを辿ります。

GradientDrawable.png

ありました!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 : 今回の僕のアプリの場合、RecyclerViewViewHolder について評価したいわけですが、RecyclerView の個々の ViewHolder アクセスには withId(...) Matcher が使えず、カスタムの Matcher を使用しています。1


感想



  • shape で定義した backgroundShapeDrawable ではなく、GradientDrawable になるっていうのは意外でした。きっと、rect や ellipse ベースではないもっと複雑な shape を定義すると ShapeDrawable になるのかな?


    • この辺はバージョンにおける実装の違いが怖いですが、一応 API level 28 (Pie) と API level 19 (Kitkat) でテストしたところ、期待通りに動きました。



  • Java や Kotlin には reflection があるので、こういう ViewMatcher を書いたりするのは、調べさえすればワンパスは通せるという感じがあります。ただ、Proguard をかますと間違いなくハマりそう。





  1. 一般的に RecyclerViewアクセスが思ったより大変でハマりました。他にも方法はあるのかもしれませんが RecyclerViewMatcher というのをコピーしてきて使いました。これを使う場合、View の Matcher には withId(...) でアクセスするのではなく、withRecyclerView(R.id....).atPosition(...) でアクセスします。