Espresso Test Recorderの導入

# はじめに
Android Studioに搭載されているUIテスト記録ツール **Espresso Test Recorder** を使用してみました。
Espresso Test Recorderはアプリの操作を記録することでEspressoでのテストコードへと自動で変換してくれる便利なツールです。
EspressoのAPIの知識がなくても直感的にテストが作成することが可能です。

以下のような、クリックすると文字が変化するありがちなサンプルコードで実際に確認していきます。
![c9e9b99255e358d28ef243b64a79407c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235269/ee5bfbe0-28dc-2580-a366-7c0d1a415636.gif)




# 実行
- Android Studioでテストしたいプロジェクトを開いておく
- テストアプリが動作する端末もしくはエミュレータを用意しておく

当然ですが、上記2つの準備が必要です。
Android Studioから端末が認識されている状態で、Run -> Record Espresso Testを選択すると記録開始です。
![fc63752ce912bb7da036ed259a214d22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235269/95ff41c8-9dee-5186-57d6-b6c1b210a405.png)


端末上でアプリが立ち上がり、Android Studio上にRecord Your Testと書かれたダイアログが表示され記録できる状態となります。
![86969b96889c936de79b17eae75e8494.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235269/a11de9b4-34c3-d18d-2e94-a68869236307.png)


# 記録
記録が始まったら、実際にテストしたい手順でアプリを操作します。
ひとつ操作するたびに、Record Your Testダイアログに操作した内容が記録されていきます。

記録時の応答速度が遅いため、ダイアログに操作内容が反映されるのを待ってから次の操作を行うようにしましょう。
間違った操作もそのまま記録されてしまうので注意が必要ですが、後ほどコードを手動で修正すればよいだけなのであまり気にする必要はありません。

# アサーション
アサーションを記録するには Record Your Test ダイアログ右下のAdd Assertionボタンを押します。
ボタンを押してしばらく待つと、右半分にテスト対象アプリの画面が表示された状態になります。
以下の操作でアサーションを記録していきます。

1. スクリーンショット上で、検証したい箇所をクリック
1. 検証対象のViewが赤枠で囲まれるため、対象が正しいか確認
1. ダイアログ左下のEdit assertion枠でアサーションを設定
1. ダイアログ左上に検証手順が記録されていることを確認
 
![d1636376206bcb5f538716a9231c6160.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235269/5d00a7a3-04ce-f25d-0761-58d7144eef28.png)





**Save and Add Anotherボタン**
アサーションを記録し、引き続き別のアサーションを記録する

**Save Assertionボタン**
アサーションを記録し、動作の記録に戻る



## アサーションの種類
### text is
Edit assertionで指定したViewのテキスト文字列と一致しているか検証
### exists
Edit assertionで指定したViewが存在していることを検証
### does not exist
Edit assertionで指定したViewが存在していないことを検証

# テストの保存
先ほどのダイアログの右下のOKを押すと、テストが自動生成されます。
テストクラス名と言語を聞かれるので入力して保存しましょう。

![cec651bae928155772750c279f5941bd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235269/d7ffba25-9f64-e59c-2714-28ad9ae3f251.png)


# 生成されたテストコード
「ボタンクリック後、指定した文字列が表示されているかどうか」のテストが自動生成されました。

```kotlin:MainActivityTest.kt
package net.storehouse.nono.espressotestrecordersample


import android.support.test.espresso.Espresso.onView
import android.support.test.espresso.action.ViewActions.click
import android.support.test.espresso.assertion.ViewAssertions.matches
import android.support.test.espresso.matcher.ViewMatchers.*
import android.support.test.filters.LargeTest
import android.support.test.rule.ActivityTestRule
import android.support.test.runner.AndroidJUnit4
import android.view.View
import android.view.ViewGroup
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.TypeSafeMatcher
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@LargeTest
@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @Rule
    @JvmField
    var mActivityTestRule = ActivityTestRule(MainActivity::class.java)

    @Test
    fun mainActivityTest() {
        val appCompatButton = onView(
            allOf(
                withId(R.id.button), withText("クリックする？"),
                childAtPosition(
                    childAtPosition(
                        withId(android.R.id.content),
                        0
                    ),
                    2
                ),
                isDisplayed()
            )
        )
        appCompatButton.perform(click())

        val textView = onView(
            allOf(
                withId(R.id.main_text), withText("ボタンがクリックされました！"),
                childAtPosition(
                    childAtPosition(
                        withId(android.R.id.content),
                        0
                    ),
                    0
                ),
                isDisplayed()
            )
        )
        textView.check(matches(withText("ボタンがクリックされました！")))

        val appCompatButton2 = onView(
            allOf(
                withId(R.id.button), withText("元に戻す"),
                childAtPosition(
                    childAtPosition(
                        withId(android.R.id.content),
                        0
                    ),
                    2
                ),
                isDisplayed()
            )
        )
        appCompatButton2.perform(click())

        val textView2 = onView(
            allOf(
                withId(R.id.main_text), withText("ボタンをクリックしてね"),
                childAtPosition(
                    childAtPosition(
                        withId(android.R.id.content),
                        0
                    ),
                    0
                ),
                isDisplayed()
            )
        )
        textView2.check(matches(withText("ボタンをクリックしてね")))

        val textView3 = onView(
            allOf(
                withId(R.id.main_text2), withText("押すとこのテキストは消えるよ"),
                childAtPosition(
                    childAtPosition(
                        withId(android.R.id.content),
                        0
                    ),
                    1
                ),
                isDisplayed()
            )
        )
        textView3.check(matches(withText("押すとこのテキストは消えるよ")))
    }

    private fun childAtPosition(
        parentMatcher: Matcher<View>, position: Int
    ): Matcher<View> {

        return object : TypeSafeMatcher<View>() {
            override fun describeTo(description: Description) {
                description.appendText("Child at position $position in parent ")
                parentMatcher.describeTo(description)
            }

            public override fun matchesSafely(view: View): Boolean {
                val parent = view.parent
                return parent is ViewGroup && parentMatcher.matches(parent)
                        && view == parent.getChildAt(position)
            }
        }
    }
}

```

# テスト結果
ちゃんとテストも通りますね。
![dbf98fe159755563a990f4e7fbcf2f5b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/235269/43e5447f-617a-de21-8c4a-2e200ce77d59.png)


# 気になったところ
- does not exist でviewが存在しないことを確認するアサーションを作るとき、スクリーンショットに存在しないViewが選べないため結局手動で作る必要がある…？
- 余計な操作が記憶され、都度消すのはやはり手間

不自由はあるけれど、シンプルなテストだったり簡単な枠組み作成くらいには使えるかも。