【Android】テスト（UnitTest/UITest）事始め

AndroidアプリのUnitTest・UITestの始め方についてまとめます。
RecyclerViewのUITestの方法や、Intentを使って条件分岐する方法も記載しています。

# Unit Tests
## 基本
Android Studioなら、簡単にテストの雛形が作成できます。
テスト対象クラスの上でCommand+Shift+Tし、説明に従って進めていくだけです。
詳しくはこの記事を見てください。
https://qiita.com/ikemura23/items/2e28d19142d36835e398

例えば、以下のようなサービスを選択してテストを作成すると、

```OSService.kt
interface OSService {
  fun detectLanguage(): Lang
}

class DefaultOSService : OSService {
  override fun detectLanguage(): Lang {
    val deviceLanguage = Locale.getDefault().toString()

    return Lang(deviceLanguage) ?: Lang.JAPANESE
  }
}
```
以下のようなテストの雛形ができます。

```/app/src/test/java/packagename/DefaultOSServiceTest.kt
import org.junit.After
import org.junit.Before
import org.junit.Test

import org.junit.Assert.*

class DefaultOSServiceTest {

    @Before
    fun setUp() {
    }

    @After
    fun tearDown() {
    }

    @Test
    fun detectLanguage() {
    }
}
```

以下では、[AssertJ](http://joel-costigliola.github.io/assertj/)と[truth](https://truth.dev/)の2つのライブラリでのテスト方法を紹介します。

## AssertJ
```javascript:build.gradle
    testImplementation 'org.assertj:assertj-core:3.10.0'
```

```DefaultOSServiceTest.kt
import org.assertj.core.api.Assertions.assertThat // ←これ

import org.junit.After
import org.junit.Before
import org.junit.Test

class DefaultOSServiceTest {
    @Before
    fun setUp() {}

    @After
    fun tearDown() {}

    @Test
    fun detectLanguage() {
        Locale.setDefault(Locale.US)
        assertThat(DefaultOSService().detectLanguage()).isEqualTo(Lang.ENGLISH)
    }
}

```

Android Studioのクラスもしくはメソッドの左にあるボタンを押すと、
![d0f1d084-40d3-afae-e691-2ea933af2deb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/a827dd65-76db-e3e0-232c-c5ef542e4d79.png)

テストが走ります。
![e5686ae8-ee04-e639-1301-4f5af2275068.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/491d470c-427b-4518-092d-2ea5a7db546f.png)


## truth
truthはGoogle製のアサーションライブラリで、AssertJと基本的な使い方は揃えられているようです。つまり、AssertJのアサーション例をほぼそのまま利用できます。

```javascript:build.gradle
    testImplementation "com.google.truth:truth:1.0"
    androidTestImplementation 'com.google.truth:truth:1.0' // 両方要る模様？
```

```DefaultOSServiceTest.kt
import com.google.common.truth.Truth.assertThat // ←これ

import org.junit.After
import org.junit.Before
import org.junit.Test

class DefaultOSServiceTest {
    @Before
    fun setUp() {}

    @After
    fun tearDown() {}

    @Test
    fun detectLanguage() {
        Locale.setDefault(Locale.US)
        assertThat(DefaultOSService().detectLanguage()).isEqualTo(Lang.JAPANESE) // AssertJと同じ！
    }
}
```

同様にテストできました。
![b560b413-d759-c3d6-6f2e-0809fb3a73ad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/8585e1d5-7a8d-1eab-82c5-dd52b9a5e228.png)

truthのほうがだいぶ速いようです。何度かやってみましたが、2倍くらいtruthのほうが速かったです。

# UI Tests
[Espresso](https://developer.android.com/training/testing/espresso?hl=ja)を使ったテスト方法を紹介します。
## 端末設定
テストをする前に、デバイスの設定を変更しておきます。
[テスト環境のセットアップ](https://developer.android.com/training/testing/espresso/setup?hl=ja#set-up-environment) - Android developers
>テストが不安定になるのを避けるため、使用する仮想デバイスまたは物理デバイスでシステム アニメーションをオフにすることを強くおすすめします。デバイスの [設定] > [開発者向けオプション] で、次の 3 つの設定を無効にします。
ウィンドウ アニメスケール
トランジション アニメスケール
Animator 再生時間スケール

## 依存
Espressoを使います。
https://developer.android.com/training/testing/set-up-project#android-test-dependencies

`androidTestImplementation`が以下のような感じになっていればOKです。

```javascript:build.gradle
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.ext:truth:1.2.0'
    androidTestImplementation 'com.google.truth:truth:1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-web:3.2.0'
    androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.2.0'
```

## テストコード
androidTest内の`ExampleInstrumentedTest`というサンプルや以下のドキュメントに倣ってテストを作ります。
https://developer.android.com/training/testing/ui-testing/espresso-testing?hl=ja#kotlin

```HomeViewTest.kt
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.filters.LargeTest
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4

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

    private lateinit var title: String

    @get:Rule
    var activityRule: ActivityTestRule<MainActivity>
            = ActivityTestRule(MainActivity::class.java)

    @Before
    fun initValidString() {
        title = "ホームタイトル"
    }

    @Test
    fun checkTitle() {
        onView(withText("アプリのホーム画面です")).check(matches(isDisplayed())) // [1]
        onView(withId(R.id.cancel_button)).perform(click()) // [2]
        onView(withId(R.id.title_text))
            .check(matches(withText(title))) // [3]
    }
}
```
このテストでは、以下のことを実行します。
[1] 画面に該当の文字列（を持つ要素）が表示されているか確認
[2] 画面上の該当IDの要素をクリック
[3] 画面上の該当IDの要素が該当の文字列を持つ（表示している）か確認

あとは▶︎ボタンを押してデバイスを選択するとテストが走ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/c7139b48-f1f9-bd30-c167-3002a770c60e.png)

通りました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124752/cab612aa-4b17-89a7-7ce7-563f5505e560.png)

起動には時間がかかりますが、テスト自体は速いです。要素を探す処理も、デフォルトで少し待ってくれながらも、見つかればすぐに次の処理を行ってくれるようです。

## RecyclerViewのテスト
[RecyclerView](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/RecyclerView.html?hl=ja)は、要素中に色々入れてリストを表示できます。
これをテストする方法を紹介します。

### 基本のテスト
デフォルトでは、中身に依存しない以下のようなテストが可能です。
1. スクロールする
2. ○番目の要素にアクションする
3. リストに期待値があるか探す（複雑な期待値の用意が大変）

ドキュメントには、「○番目の要素をクリックした後に表示を確認する」例があります。
https://developer.android.com/training/testing/espresso/lists#recycler-view-list-items

```kotlin
@Test fun scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
        .perform(
            RecyclerViewActions.actionOnItemAtPosition(
                ITEM_BELOW_THE_FOLD,
                click()
            )
        )

    // Match the text in an item below the fold and check that it's displayed.
    val itemElementText = "${activityRule.activity.resources
        .getString(R.string.item_element_text)} ${ITEM_BELOW_THE_FOLD.toString()}"
    onView(withText(itemElementText)).check(matches(isDisplayed()))
}
```

### 複雑なテスト
もしさらなる複雑なテストをしたい場合、例えば中身の変化などを知りたいなら、[Adapter](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/RecyclerView.Adapter.html?hl=ja)かRecyclerView用のMatcherを拡張する必要があるでしょう。

基本的にはMatcherを拡張するのがよいようです。
[EspressoでRecyclerViewの中身を比較](https://qiita.com/AAkira/items/26ee7b18b11635cfc621) - Qiita
[EspressoでRecyclerViewのチェックを行うカスタムMatcherを作成](https://qiita.com/umechanhika/items/d4aa60be52c47e315799) - Qiita

## Intentのextraを使って条件分岐する
テスト開始前に[Intent](https://developer.android.com/reference/kotlin/android/content/Intent.html)にextraを設定することで、条件分岐に使えます。
コードの下に引数等が分かりにくいものを記しておきます。

```HomeViewTest.kt
    @get:Rule
    var activityRule: ActivityTestRule<MainActivity>
            = ActivityTestRule(MainActivity::class.java, false, false)

    @Before
    fun setUp() {
        var intent = Intent()
        intent.putExtra("UI_TEST", true)
        activityRule.launchActivity(intent)
    }
```

```MainActivity.kt
if (instance.intent.getBooleanExtra("UI_TEST", false)) {
    // "UI_TEST"がtrueのとき
} else {
    // それ以外
}
```
- `ActivityTestRule(activityClass: Class<T>, initialTouchMode: Boolean, launchActivity: Boolean)`  
`launchActivity`に`false`を渡すとテスト開始時にActivityが自動で起動しなくなるため、独自のIntentを設定できます。
- `ActivityTestRule.launchActivity(startIntent: Intent)`  
テスト下でActivityを起動します。extraを設定したIntentを渡せば、テスト中にextraを取得できます。
- `Intent.getBooleanExtra(name: String, defaultValue: Boolean): Boolean`  
Intentに設定したBooleanのextraを取得します。`defaultValue`は`name`に該当するextraがなかった場合に返す値を渡します。

# まとめ
- AssertJやtruthを使ってUnitTestを試しました。
- Espressoを使ってUITestを試しました。