Android, Kotlin: テストコードを書く

- Minimum SDK: API 23
- Android Studio 4.0.1
- Kotlin 1.4.0

以前、Androidのテストを、UnitTestにて実施しようとしました。
※[AndroidでのUnitTestざっくり入門](https://qiita.com/nozaki-sankosc/items/538fe9b2a13aa7403df6)

ですが、Realmが絡んだテストが実施できず、またRealmの実行を含んだオブジェクトのモック化もうまくいきませんでした。（元の処理が実行されてしまいました。）

なので、方針を変更しInstrumentedTest(androidTest)の方でテストケースを実施するようにします。

やりたいことは次のこととなります。

- 表示の確認
- 入力値の確認
- データベースに格納されているかの確認
- 画面遷移：次のActivityにIntentが渡されているかの確認

## 準備

Realmの準備はこちらを参照：[Android,KotlinでRealm](https://qiita.com/nozaki-sankosc/items/35516da505ca4f0655ec)

一番上は、プロジェクトを作成した時に既に入っていました。

```gradle:bundle.gradle(app)
dependencies {
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'
}
```

## テスト対象のアプリ

Viewは省略しますが、次のようなアプリをテスト対象とします。

- textView: 初期表示は`Hello World!`
- editText: 入力フォーム
- button1：textViewの内容書き換え
- button2: editTextの内容をデータベースに保存
- button3: 次の画面(SecondActivity)に、editTextの内容を渡して遷移
- SecondActivity: 受け取ったテキストをtextViewに表示します。

```kotlin:MainActivity
class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		findViewById<Button>(R.id.button1).setOnClickListener {
			findViewById<TextView>(R.id.textView).text = "Click"
		}

		findViewById<Button>(R.id.button2).setOnClickListener {
			val text = findViewById<EditText>(R.id.editText).text.toString()
			Realm.getDefaultInstance().executeTransaction {
				it.copyToRealm(Data(UUID.randomUUID().toString(), text))
			}
		}

		findViewById<Button>(R.id.button3).setOnClickListener {
			val text = findViewById<EditText>(R.id.editText).text.toString()
			val intent = Intent(this, SecondActivity::class.java)
			intent.putExtra("text", text)
			startActivity(intent)
		}
	}
}
```

```kotlin:Data
open class Data(
    @PrimaryKey var id : String = "",
    var value: String = ""
) : RealmObject()
```

## テストケース：前処理

テストケースの前処理でRealmをメモリで動かす設定をしておきます。
メモリで動かす設定にしていてもファイルは作られるみたいで、名前を毎回変更しないとエラーになってしまいます。

また、テスト対象のActivityをIntentsTestRuleで定義します。


```kotlin
@RunWith(AndroidJUnit4::class)
class SampleInstTest {
    @Before
    fun init() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        Realm.init(context)
        val builder = RealmConfiguration.Builder()
        builder.inMemory()
        builder.name(UUID.randomUUID().toString())
        Realm.setDefaultConfiguration(builder.build())
    }

    @get:Rule
    val intentRule = IntentsTestRule(MainActivity::class.java)
}
```

## テストケース：表示の確認

初期表示の確認と、ボタンクリック時に表示が変更されているかの確認です。

```kotlin
    @Test
    fun changeTextView() {
        // 初期表示
        onView(withId(R.id.textView)).check(matches(withText("Hello World!")))

        // ボタン１クリック：表示切り替え
        onView(withId(R.id.button1)).perform(ViewActions.click())
        onView(withId(R.id.textView)).check(matches(withText("Click")))
    }
```

## テストケース：入力値の確認と、データベースの確認

Realmのオブジェクトは、データ登録後に取得しないと反映されていませんでした。

```kotlin
    @Test
    fun saveData() {
        // テキスト入力
        onView(withId(R.id.editText)).perform(ViewActions.typeText("InputTest"))

        // ボタンク２リック：データベースに保存
        onView(withId(R.id.button2)).perform(ViewActions.click())

        // 入力したデータが一つ入っていることを確認
        var realm = Realm.getDefaultInstance()
        Assert.assertEquals(1, realm.where(Data::class.java).count())
        val data = realm.where(Data::class.java).findFirst()
        Assert.assertEquals("InputTest", data?.value ?: "")
        realm.close()

        // もう一回クリックで、データが増えていることを確認
        onView(withId(R.id.button2)).perform(ViewActions.click())
        realm = Realm.getDefaultInstance()
        Assert.assertEquals(2, realm.where(Data::class.java).count())
        realm.close()
    }

```

## テストケース：画面遷移

```kotlin
    @Test
    fun callAcivity() {
        // テキスト入力・ボタン３クリック
        onView(withId(R.id.editText)).perform(ViewActions.typeText("NextActivity"))
        onView(withId(R.id.button3)).perform(ViewActions.click())

        // 遷移先のActivityと、Intentでデータが渡されているかを確認
        intended(allOf(
            hasComponent(hasShortClassName(".SecondActivity")),
            hasExtra("text", "NextActivity"))
        )
    }
```

## テストケース：遷移先

遷移先の確認は、テスト対象のActivityが変わるため別クラスで行います。
こちらのテスト対象は、ActivityTestRuleで指定します。

```kotlin:SecondInstTest
@RunWith(AndroidJUnit4::class)
class SecondInstTest {
    @get:Rule
    val activityRule = ActivityTestRule(SecondActivity::class.java)

    @Test
    fun startNextActivity() {
        // Intentを渡してActivity起動
        val intent = Intent()
        intent.putExtra("text", "PassedText")
        activityRule.launchActivity(intent)

        // 表示確認
        onView(withId(R.id.textView)).check(matches(withText("PassedText")))
    }
}
```

## TestRuleについて

- ActivityTestRule: 基本のルール
- ActivityScenarioRule: Activityの起動、終了など自動でやってくれます。基本はこれを使用すればいいと思います。今回は使ってませんが。
- IntentsTestRule: 画面遷移を伴うテストの時に使用します。

## その他

またの機会に、ListViewなどのテストも追記していきます。