FatでjavaなActivityのリファクタの進め方を検討したメモ

はじめまして。リファクタ勉強中のAndroidエンジニア5年生です。
テストがないFatなActivityのリファクタを進めるうえで検討した内容を残しておきます。
テストがないようなプロジェクトしか経験しておらずこの辺は勉強し始めたばかりです。

いまはまだ小さめのクラスで試しただけのものなので、実際に運用するなかで得た知見があれば追記していきます。

ご意見・ご指摘あればお伝えいただけるととても嬉しいです。

# 背景

- 機能追加にあたってリファクタが必要だった。
- ロジックとかはActivityが持ってしまっている。
- javaで書かれているのでKotlinにしたい。
- テストがないのでテストを書きたい。

# リファクタの流れ

1. リファクタ対象のActivityが動くシナリオでUIテストを書く。
2. Activityの子クラスを作り、既存コードからはそちらを参照させる。親クラスは抽象クラスにしておく。
3. ロジックはViewModel以下へ、UIの操作はすこしずつKotlin化して子クラスへ移動させていく。ViewModelにはテストを記述していく。

## 流れ解説

1. 動作を保証しながら進めていきたいので前もって保証するべき内容を明確にしておきます。
2. [局所的拡張の導入](https://www.refactoring.com/catalog/introduceLocalExtension.html)をAndroidに持ってきたような感じの考え方です。既存Activityの動作を担保しながらすこしずつKotlin化やクラスの抽出を進めたいという意図です。微妙な状態のActivityを使えないようにリファクタ対象は抽象クラスにしてみました。
3. 最終的にはテストで動作が保障されたコードにしたいので、ここでUTを書きながらリファクタを進めていきます。初めに記述したUIテストはリファクタが終了すれば消してもよいかもしれませんが、ここで書いたUTはCIなどに組み込まれて資産として残ることを想定しています。

# 感想

- UIテストはテストレコーダーが思ったより優秀で作成しやすい。
- 作成する内容を考えるのは大変。経験が必要そう。
- UIテストが成功していれば、基本の動作は担保できているということなので、(比較的)心の余裕をもってリファクタできた。
    - 修正したらUIテストを実行して落ちたら確認すればいいので自分で動作確認したり確認漏れを疑ったりする必要がなかったので割と快適。
    - 対象が複雑な機能を持った画面で、確認項目が多い場合は、人の手でやるより確認漏れがなくなってよさそう。UIテストをどこまで網羅するかはバランス感覚を磨く必要がありそう。
- PRにテスト結果を添付すればレビュアーに客観的な確認結果を伝えやすそう。
- 端末が重かったりすると`performClick()`(後述)が長押しになっちゃったりして安定しない部分もあった。

# 手順

## 1. UIテスト作成

- Espressoを使用しました。Appiumもありでしたが環境構築に手間がかかりそうだったのでやめました。
- `Command + Shift + t`で新規テストを作成します。UIテストなので/androidtest配下に作成します。
- [Espresso の依存を追加](https://developer.android.com/training/testing/ui-testing/espresso-testing?hl=ja#setup) します。
- `Run>Record Espresso Test`で手順の記録を開始します。
- 実行
    - 謎エラー発生
        - `Class 'org.hamcrest.StringDescription' does not implement interface 'java.lang.Iterable' in call to 'java.util.Iterator java.lang.Iterable.iterator()'`
            - Espresso3.5.0-alpha06 のバグっぽいです。レコード開始するときに足りてない設定があったっぽく、それを自動で追加するときにアップデートされてしまったらしいです。3.4.0に戻して解決。
- 再度実行
    - 失敗。
        - `androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching:`
        - スプラッシュスクリーンですぐ操作しようとして落ちてました。
            - 該当のViewが表示されるまで待機させて回避します。

```diff
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
```

```diff
+ instance.wait(Until.findObject(By.desc("hoge")), 5000L)

val textView = onView(
    allOf(
        withId(R.id.hoge), withText("hoge"),
        withParent(withParent(IsInstanceOf.instanceOf(android.widget.RelativeLayout::class.java))),
        isDisplayed()
    )
)
```

- 同様に、画面遷移や通信の結果を待たずに操作しようとして落ちるケースがあるので、似たような対応を各所にぶっこみます。
- 保障すべき動作仕様についてテストが通ればリファクタ開始します。

## 2. 子クラス作成

- リファクタ対象と同名の子クラスを作成してリファクタ対象を適当な名前に変更します。リファクタ対象は抽象クラスにします。
- 対象クラスの名前を担保したければ、`Shift + F6`でリファクタ対象クラスとその参照をリネームして、リネーム後と同名の子クラスを作成後、リファクタ対象クラスだけ元の名前に戻せば、安全に変更できると思います。
- この親クラスはリファクタ完了時に消えてなくなる想定ですのでリファクタ中の一時的な状態であることがわかるような名前が付けられるとよいと思います。

## 3. リファクタ

- 粛々と移動させていきます。移動のたびにテストを実行します。テストが実行されている間はTwitterを見ます。
- テストが落ちていれば移動に失敗したということですが端末やエミュレータの状態によって`performClick()`が長押しとして動いてしまって失敗するといったこともまれにあります。

### 3.1 UT作成

- ViewModelに移動させたロジックに関してはUTを記述していきます。
- テストの内容はまちまちだと思いますのでとくに触れません。
- 環境構築(とくにcoroutinesまわりとRetrofitのモック化)については多少ハマった部分がありましたが以下の記事が参考になりました。

#### coroutines のテスト

- 公式
    - [https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/)
- UnitTest環境はLooperを持っていないので、CoroutineのDispatcherにテスト用のものを設定してやらないとExceptionが吐かれる
    - [https://stackoverflow.com/questions/58303961/kotlin-coroutine-unit-test-fails-with-module-with-the-main-dispatcher-had-faile](https://stackoverflow.com/questions/58303961/kotlin-coroutine-unit-test-fails-with-module-with-the-main-dispatcher-had-faile)
    - 公式の例
        - [https://github.com/android/architecture-components-samples/blob/main/LiveDataSample/app/src/test/java/com/android/example/livedatabuilder/util/MainCoroutineRule.kt](https://github.com/android/architecture-components-samples/blob/main/LiveDataSample/app/src/test/java/com/android/example/livedatabuilder/util/MainCoroutineRule.kt)
- LiveDataをObserveしてから通知されるのを待っていないと、処理の結果を得る前にテストが終了してしまい、失敗する
    - 公式のサンプル(Utilクラス)
        - [https://github.com/android/architecture-components-samples/blob/main/LiveDataSample/app/src/test/java/com/android/example/livedatabuilder/util/LiveDataTestUtil.kt](https://github.com/android/architecture-components-samples/blob/main/LiveDataSample/app/src/test/java/com/android/example/livedatabuilder/util/LiveDataTestUtil.kt)
- coroutines 1.6 以降のバージョンだと、テスト用のDispatcherを設定する `Dispatchers.setMain` が動かない。
    - `@RunWith(AndroidJUnit4::class)`　を明記してやると解決
    - [https://github.com/Kotlin/kotlinx.coroutines/issues/3244#issuecomment-1096377497](https://github.com/Kotlin/kotlinx.coroutines/issues/3244#issuecomment-1096377497)

#### API mocking

- [https://qiita.com/toranokopg/items/ae84840bac548977233c](https://qiita.com/toranokopg/items/ae84840bac548977233c)
- [https://qiita.com/ikeda_shogouki/items/d81d458289edfbd8d162](https://qiita.com/ikeda_shogouki/items/d81d458289edfbd8d162)