MVVM + LiveData + coroutine アーキテクチャにSpek2を使って資産価値のあるUnitTestを追加していく
この記事は Voicy Advent Calendar 2020 2日目の記事です。
前日は @shiei_kawa さんの Swagger/OpenAPIの定義ファイルからCircleCIでスタブサーバーを自動生成して開発を高速化する でした。
明日は @somen440 さんの貧乏エンジニアが考える極力お金をかけないモダンなサーバーサイド環境 2020 です。
背景
既存のAndroidプロジェクトへ Unitテスト を追加してみて得られた情報をまとめておきます。
開発環境
Android Studio 4.1
Kotlin 1.3.71
Gradle 5.6.4
Gradle Plugin 3.6.3
アーキテクチャ
MVVMを採用しています。
詳しくはアプリ アーキテクチャ ガイドを参照ください
参考
DroidKaigi 2020で公開してくださっている、こちらを参照いただくのが手っ取り早いです。
- DroidKaigi 2019 — Unit test for ViewModel and LiveData / hkusu [JA] https://www.youtube.com/watch?v=4t734E4TKsk&t=477s
- MockK 公式サイト
https://mockk.io/#object-mocks
Spekとは
Kotlin製の、Kotlinのための BDD テストフレームワークです。
Spekを導入すると、検証したいプログラムの振る舞いや仕様を記述していく記法を提供してくれます。
RSpecライクにDSLを利用し記述していけるということらしいです。
DroidKaigi2019でSpekに関するセッションがあり、参画しているプロジェクトで使ってみたく導入しようと考えました。
例えば、ユーザー状態の判定を担うUserStateクラスがあります。UserStateクラスでは複数のフラグを要素に持っていて、未ログインだったり、ログイン済みだったり、とある機能が使用可能だったり、不可能だったりを複数のフラグからユーザーの状態を判定しています。
@Test
fun xxxfeatureFlag_1_ユーザーの状態が〇〇であること() {
state.setxxxfeatureFrag(1)
assertEquals(STATE_〇〇, state.userState)
}
上記のようなJunitテストでは、条件ごとに並列で追加していき以下のようになります。
Spekでは階層に分けられるので、大項目→中項目→小項目というようにカテゴライズすることができ、テスト仕様そのものを書き表しやすいように思います。
Spekの導入
公式のセットアップ情報を参照してください。
私の環境では公式のセットアップを見てもビルドが通らなかったので参考として載せます。junit-vintageはJUnit 4で書かれた昔のテストを 新しいJUnit Platform(JUnit5)上で実行するテストエンジンです。
Spekを使ったUnitテスト
- Spekを継承する
- コンストラクタのクロージャー内にテストを記述していく-
- beforeEachTest/afterEachTest に事前処理 / 事後処理を記述
- describeまたはcontextで関連する仕様をグループ化
- 4のクロージャー内に it でグループ化し、アサーション/検証を配置する
ViewModelへのUnit Test を追加するにあたってはまったこと
ViewModelのテストを追加した際 java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. のエラーが発生しました。
テストではメインスレッドがないということのよう?自信ない…
勧められた通り Dispatchers.setMain を使って実行スレッドを切り替えます。他で使えるよう拡張関数を定義します。
LiveDataの Unit Testの書き方 / 悩んだところ
LiveDataを監視し更新があったら値に応じて何らかの処理を行っています。
特に意識せず次のような UnitTest を書くと、Looper が mock されていないというエラーがでます。
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.
viewModelのテスト対象を実行して完了を待たずに値の検証をしてしまうようです。
AACのcore-testingを導入してTaskExecutorをカスタムし解決します。
isMainThreadで常にtrueを返しています。
高階関数のモックで悩んだ
APIリクエストやデータアクセスを担うRepositoryではKotlinの高階関数が使用されていました。
ViewModelでRepositoryから得た値に応じて何らかの処理をしているためRepositoryの高階関数をモックする必要がありました。
Kotlinの高階関数をモックする方法 を参考にレスポンスをモックをしました。
まとめ
- Spekの導入に1日ハマった
- ちょっとしたテストを書くのにAndroidの仕様なのかバグなのかはっきりしないことでつまづくのでツラミがあるけど、超えなければならない壁である。
- テスト対象としたい条件分岐や状態参照などのビジネスロジックがActivityやFragment、DataBindingを用いてLayoutファイルに実装されているため、タイミングを未図ってテストフレンドリーにリファクタしたり、クラスやオブジェクトの垣根を超えてモックするオブジェクトを切り替えつつテストを書いていくことになりそう。
- t_wadaさんの質とスピードのセッションに触発されるも、スピード優先という現場で質を保ちつつこなしていくには高度なスキルが要求されるなとしみじみ思う。チームと話し合いながら保守タスクを定期的に入れていきたい。