LoginSignup
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-01-21

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

問題

下図のように、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(...) でアクセスします。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1