AndroidでE2Eやってみる

[Ateam Finergy Inc.× Ateam CommerceTech Inc.× Ateam Wellness Inc. Advent Calendar 2022](https://qiita.com/advent-calendar/2022/finergy-commerce-tech-wellness) の14日目は株式会社エイチームウェルネス @yothio が担当します

レガシーなAndroidアプリ/最新のアーキテクチャを上手く追従できていないAndroidアプリを保守していこうと思った時「ロジックが正しいか」といったテストを書こうとしても、Viewとロジックが分離されていないことがあるためユニットテストを書き進めるのは難しく「見た目上変更がない」「アプリケーション全体で見た時に不具合が気づけるように」という思いの元、E2Eテストから先に作ってみようと思いました

# 初めに
最近Webばっかり書いていたのもあり「AndroidでのE2Eは難しそう、あんまりイメージがつかない」と漠然に思っていましたが、実際に触ってみると簡単にできたので「難しいと思って手を出したことがない人」に届くと嬉しく思います
「 :thinking:  そもそもE2Eテストってなんだっけ？」といった
E2Eテスト自体については今回は省かせていただきます :pray:

## Espressoの導入
Androidの公式にも書いてある通りのUIテストライブラリのEspressoを使用してE2Eテストを進めます
基本的には公式ドキュメントに書いてあるとおり進めますが一部非推奨のものなどあるので適宜変更していきます

https://developer.android.com/training/testing/espresso?hl=ja

導入の時にやることとしては
1. ライブラリの追加
2. テスト実行時のランナーの指定
3. テストファイルとコードの作成

### ライブラリの追加
Espressoのドキュメントに書かれている通りに`testInstrumentationRunner`の項目追加と下記のライブラリを追加します。
ただ、執筆時のEspressoのドキュメントではDeprecateになっているサンプルコードが存在するので、回避するた`androidx.test.ext:junit-ktx`も追加します。

```kotlin:build.gradle(:app)
android {
    compileSdk 33

    defaultConfig {
        applicationId "com.example.e2etest"
        minSdk 26
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // <<<----- こちらがなければ追加
    }
    ....
}

dependencies {
  .....

  androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
  androidTestImplementation 'androidx.test:runner:1.4.0'
  androidTestImplementation 'androidx.test:rules:1.4.0'
}
```

### テストファイルの追加
プロジェクトの新規作成時に`app/src/androidTest/java/com/~~~`とandroidTestのディレクトリが作成されているはずですがなければ作りましょう
作成したディレクトリにテストファイルを作成します
今回は例として下記の名前で作成しました
`app/src/androidTest/java/com/example/e2etest/HelloEspressoTest.kt`

### テストコードの追加
Espressoのサンプルと基本的に同じですが、`ActivityTestRule`がdeprecateとなっているので`activityScenarioRule`に置き換えています。
こちらが先ほどライブラリ追加であった`androidx.test.ext:junit-ktx`を追加する理由となります。

```kotlin:activityScenarioRule使用したテスト
package com.example.e2etest

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

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

    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()

    @Test
    fun listGoesOverTheFold() {
        onView(withText("Hello world!")).check(matches(isDisplayed()))
    }
}
```


:::note info
`activityScenarioRule`については「Espressoのサンプルコード」ではなく
↓の「アプリのアクティビティをテストする」にて使用されているのでこちらを参考する方が良いかも
:::

https://developer.android.com/guide/components/activities/testing?hl=ja

テストを実行するためには、ActivityもしくはFragmentを起動する必要があります
そのため`activityScenarioRule`を使用してMainActivityを起動します
上記のサンプルコードのように`@get:Rule`の対象として`activityScenarioRule`を指定してますが
アクティビティの起動はテストメソッド内で`launchActivity`を実行しても同じ結果が得られます

<details><summary>launchActivity使用したテスト</summary>

```kotlin

@RunWith(AndroidJUnit4::class)
@LargeTest
class HelloEspressoTest {
    @Test
    fun listGoesOverTheFold() {
        launchActivity<MyActivity>()
        onView(withText("Hello world!")).check(matches(isDisplayed()))
    }
}
```
</details>

同一のファイル内に複数テストを書く場合は`activityScenarioRule`で書いておくと自動的にアクティビティが立ち上がるので基本的にそちらで良いと思います

## テスト書いていく
これまでの手順でテストが動作するようになったので後は好きにE2Eを書いていくだけです
Espressoが提供しているメソッドを利用して、Viewの要素を`ViewInteraction`として取得し、その要素に対して検証していきます

サンプルコードの例だと
withTextにて「Hello World!」と表示しているViewを取得し
onViewでwithTextの時に取得したViewのViewInteractionを作成し、表示されているかを検証しています

```kotlin
onView(withText("Hello world!")) // Viewの要素を取得
  .check(matches(isDisplayed())) // 要素の検証
```

また、Viewの要素を取得できるため`EditText`なら入力
`ClickListener`を登録しているならクリックさせて要素を検証してあげると良いでしょう

```kotlin
onView(withId(R.id.button_first))
  .perform(click())
```

## 注意

さまざまな背景によって、

> フラグメントは、特定の親アクティビティまたはフラグメントに依存してはなりません。

https://developer.android.com/guide/fragments/test?hl=ja

スマートフォンアプリに関わるのが久しぶりだったので誤っている箇所やより良い方法などあればコメントや編集リクエストをいただけると助かります

## 最後に
Android Developersに書かれていますがFragmentはActivityに依存しない設計で組んであれば全体を通してテストをせずにFragment単体でテストすることを推奨していますが

ActivityとFragmentの結合度が高くなっていたりと、上手く分離できていないケースがある場合はActivityを通じてE2Eテストを行い
「```テストが通る → リファクタリング → テストが通る → Viewとロジックの分離 → テストが通る → ...``` 」といったように徐々にシステムを変更していくと安全に改善していけるかと思います

サンプルと作成したコードはこちらのリポジトリに上げてるのでご参考ください
https://github.com/yothio/android-e2e-sample

またEspressoの公式ドキュメント下部にサンプルプロジェクトのリンクがあり、そちらのテストの方が大変参考になるため一度見てみると良いかと思います
https://github.com/android/testing-samples/tree/main/ui/espresso