Androidのテストでだけ使うライブラリモジュールを作る

# 概要

[Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(16)ライブラリバージョンアップ2021春(2)](https://qiita.com/kasa_le/items/3fc4814159e20f25c59a)

上記の記事にて、KotlinでAndroidアプリを作るシリーズを作っていますが、InstrumentationTest(androidTestフォルダのテスト)とRobolectric版のテストを同時に作っていて、いわゆるUtil系のクラスや関数が双方に必要でコピペしていくのが面倒になってきました。

テスト用の共通コードを共有する方法をとれば、コードのコピペは減らせるのですが、build.gradleのライブラリの記述が長くなってきて、テスト用のライブラリだけで長くなってしまい、本体のライブラリが分かりづらくなってきました。

そこで、テストに関連するコードを共通モジュール化することをやってみたので、その覚え書きです。

Android向けのライブラリモジュールを作る方法はいくらでも探せばありますが、私が探した限り、テストに使う用途のライブラリの作り方って見つからなかったので、まあ少しは需要があるかなと言うことで、参考になれば幸いです。

結論だけ欲しい方は最後をご覧下さい。

# 環境

|ツールなど|バージョンなど|
|:--|:--|
| MacbookPro |macOS Catalina 10.15.7|
|Android Studio |4.1.2|
|Java(JDK) | openjdk version "11.0.10" |

# 目的
- テスト用のUtil系処理をライブラリモジュール化する
- 本体アプリモジュールのテスト用の依存関係をスッキリさせる

# テスト関連の依存ライブラリ

元のアプリのコードは以下にあります。

https://github.com/le-kamba/qiita_pedometer/tree/feature/qiita_16

アプリモジュールの`build.gradle`では、これだけのテスト関係の依存ライブラリがありました。
EspressoとRobolectricとMockitoが大きな依存ライブラリになっていますが、Androidのテスト用パッケージも細々としたものがあって、`testImplementation`と`androidTestImplementation`の双方に必要なために重複して宣言せねばならず長くなっています。

```app/build.gradle
dependencies {

    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.assertj:assertj-core:3.19.0'
    testImplementation 'androidx.test.ext:junit:1.1.2'
    testImplementation 'androidx.test:runner:1.3.0'
    testImplementation 'androidx.test:rules:1.3.0'
    testImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    testImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
    testImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
    testImplementation 'org.robolectric:robolectric:4.5.1'

    androidTestImplementation 'androidx.test:runner:1.3.0'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'org.assertj:assertj-core:3.19.0'
    androidTestUtil 'androidx.test:orchestrator:1.3.0'

    // FragmentTest
    testImplementation 'androidx.test:core:1.3.0'
    androidTestImplementation 'androidx.test:core:1.3.0'
    // Robolectric用,test向けではなくdebug向けに必要
    debugImplementation 'androidx.fragment:fragment-testing:1.3.2'
    debugImplementation 'androidx.test:core:1.3.0'
    // 無くてもアプリの実行は問題ないがテストがビルド出来ない
    debugImplementation "androidx.legacy:legacy-support-core-ui:1.0.0"
    debugImplementation "androidx.legacy:legacy-support-core-utils:1.0.0"
    
    // FragmentTest
    implementation 'androidx.test:core:1.3.0'
    
    // Mockito
    testImplementation 'org.mockito:mockito-core:3.7.7'
    testImplementation 'org.mockito:mockito-inline:3.7.7'
}
```

これを、可能な限りごっそりライブラリモジュールに移して、アプリ側はすっきりさせたいですね。
ということで、早速ライブラリモジュールを作っていきます。

# ライブラリモジュールを作成する

## 1. ライブラリモジュールを新規に追加する
Android Studioのメニューからライブラリを作成します。

メニューの**File-New-New Module**とします。

![new_module.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/54b4a55a-26fa-751d-9fd6-e7869369aa11.png)

**Android Library**を選んで、[Next]をクリック

![select_android_library.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/c6032ca1-662a-e4fd-41ad-dcc193ddb712.png)

任意のモジュール名を入力して、[Finish]をクリック
- 必要があれば、[Edit]をクリックしてパッケージ名を変更

![input_library_name.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/4f893e83-1264-2d53-841f-19f88daa1553.png)

モジュールが追加されます。(少し時間がかかるかも)

![library_add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/f4d9b501-b466-49d8-9694-1a87fa51b45b.png)

ここまでは、普通のライブラリモジュールを作るのと変わりありません。
まあ、ぶっちゃけこれ以降もそうなんですが。

## 2. dependenciesの設定

テストライブラリモジュールの`build.gradle`は以下のようにしました。

```myTestLibrary/build.gradle
plugins {
    id 'com.android.library'
    id 'kotlin-android'
}

android {
    compileSdkVersion 29
    buildToolsVersion "30.0.3"

    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.3.0'

    api 'junit:junit:4.13.2'
    api 'androidx.test.ext:junit:1.1.2'

    api 'androidx.test:runner:1.3.0'
    api 'androidx.test:rules:1.3.0'

    api 'org.assertj:assertj-core:3.19.0'
    api 'androidx.test.espresso:espresso-core:3.3.0'
    api 'androidx.test.espresso:espresso-contrib:3.3.0'
    api 'androidx.test.espresso:espresso-intents:3.3.0'

    def arch_version = "2.1.0" // バージョン定義を別出し
    api("androidx.arch.core:core-testing:$arch_version") {
        exclude group: 'org.mockito', module: 'mockito-core'
    }

    // RecyclerView
    implementation 'androidx.recyclerview:recyclerview:1.2.0-rc01'
    // ViewPager2
    implementation 'androidx.viewpager2:viewpager2:1.0.0'

    // FragmentTest
    api 'androidx.test:core:1.3.0'

    // Mockito
    api 'org.mockito:mockito-core:3.7.7'
    api 'org.mockito:mockito-inline:3.7.7'
}
```

`implementation`と`api`の違いですが、`implementation`は「このモジュールのビルドにだけ使う。さらにこのモジュールを参照するモジュールには引き継がない」という場合に使います。
`api`は逆に、「このモジュールのビルドにも、さらにこのモジュールを参照するモジュールも依存する」という場合に使います。
アプリモジュールでは実質的にどちらを使っても良いのですが、ライブラリモジュールとなると使い分けが必要になるので要注意です。
今回、アプリモジュールのほうの`dependencies`を減らしたかったので、`testImplementation`と`androidTestImplementation`で共通していたものはごっそりライブラリ側に`api`で移動し、アプリモジュールのほうからは削除することにしました。

なお、ライブラリモジュールの方で`testImplementation`とか`androidTestImplementation`とかを使っていませんが、これは参照するアプリ側でそれぞれ指定すればいい良いようで、でライブラリでは使用していません。

## 3. Test用Util系処理を移植する

Espresso系のUtilクラス/関数、LiveData関係のUtilクラス/関数、Mockito(かつKotlin)向けの関数を移植(ファイルをコピー)しました。

![lib_files.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/531f44df-8c20-5e2e-8b0a-defd99369c79.png)

それぞれのファイルの中身は、まとめに記載してあるリポジトリをご参照下さい。


# アプリ側の変更

## 1. dependencies設定の変更

```app/build.gradle
    testImplementation 'org.robolectric:robolectric:4.5.1'
    testImplementation project(path: ':myTestLibrary')

    androidTestImplementation project(path: ':myTestLibrary')
    androidTestUtil 'androidx.test:orchestrator:1.3.0'
    
    // 以下は消してはダメ----
    // Robolectric用,test向けではなくdebug向けに必要
    debugImplementation 'androidx.fragment:fragment-testing:1.3.2'
    debugImplementation 'androidx.test:core:1.3.0'
    // 無くてもアプリの実行は問題ないがテストがビルド出来ない
    debugImplementation "androidx.legacy:legacy-support-core-ui:1.0.0"
    debugImplementation "androidx.legacy:legacy-support-core-utils:1.0.0"
```

ライブラリ側の依存関係で`api`としたものはアプリ側からはごっそり削除し、代わりにそれぞれのテスト用に`myTestLibrary`を追加します。
アプリの`build.gradle`が少しスッキリして見やすくなりました。

## 2. 元々あったファイルを削除
ライブラリにコピーしたファイルは削除します。

## 3. import等の変更

ライブラリに移植したクラス、関数のパッケージが変わっている場合は、利用しているテストクラスでのimportの修正が必要です。
私は面倒なんで、ビルドしてみてエラーが起こったところから片っ端から直すやり方でやりますw

## 4. ビルド、テスト

ビルドが通るようになったら、テストを実行しておきます。

あ、このライブラリモジュール側のコードに対するテストは書いていません。
本来なら書くべきですかね・・・
書けないのもありそうですが。


### 4.1　単体テスト

単体テスト/Robolectricテストを実行します。Android Studioからだけでなく、CIでの実行も考慮してコマンドラインからも実行しておきましょう。
(どちらかでだけビルドエラーになったりする場合があります)

```console
# 単体テスト
$ ./gradlew app:testDebugUnitTest
```


### 4.2 Instrumentationテスト

同様に、Android Studioからだけでなく、コマンドラインからも実行してみます。
実行する前には、実機を繋いでおくかエミュレーターを起動しておくのをお忘れなく。

```console
$ ./gradlew app:connectedCheck
```

実は、ここでビルドエラーが起きました。

```console
D8: com.android.tools.r8.a: MethodHandle.invoke and MethodHandle.invokeExact are only supported starting with Android O (--min-api 26)
```

エラーメッセージでググって、以下に辿り着きました。

https://github.com/mockito/mockito/issues/2007

**Mockito**が依存している**objenesis**というライブラリとのバージョンコンフリクトのようです。
最後の方までコメントを頑張って読んでいくと、3.9.0で直されたようなので、mavenリポジトリ等を確認してみると、公開済みになっているようなので、(build.gradleで参照しているのはjcenterですが)、さっそくバージョンを上げてみます。

```app/build.gradle
    // Mockito
    api 'org.mockito:mockito-core:3.9.0'
    api 'org.mockito:mockito-inline:3.9.0'
```

これでテストをビルド、実行してみると、エラーが起きず無事テストが実行されました。

## 5. 依存関係を確認してみる

*myTestLibrary*が、テスト以外のビルドで使われていないことをちょっと確認してみようとおもいます。
Gradleの`dependencies`コマンドで依存関係を吐き出してくれますが、そのままだとターミナルにずらずらっと吐かれて見づらいので、たいていファイルにパイプして見ています。

```console
$ ./gradlew app:dependencies > deps.txt
```

このとき作成した`deps.txt`をうっかりコミットしてしまうことが多いのでお気を付けてw

`deps.txt`内を`myTestLibrary`でgrep検索を掛けてみます。
以下のキャプチャーはMacでは定番の(?)テキストエディタCotEditorでのgrep結果です。

![coteditor_grep.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/efa73ed8-761e-8fb1-13b7-8e09e3716113.png)

検出された箇所を1箇所ずつチェックしましたが、以下のコンフィギュレーション下にしか検出されていませんでした。

- androidTestImplementation
- androidTestImplementationDependenciesMetadata
- debugAndroidTestCompileClasspath
- debugAndroidTestImplementationDependenciesMetadata
- debugAndroidTestRuntimeClasspath
- debugUnitTestCompileClasspath
- debugUnitTestImplementationDependenciesMetadata
- debugUnitTestRuntimeClasspath
- releaseUnitTestCompileClasspath
- releaseUnitTestImplementationDependenciesMetadata
- releaseUnitTestRuntimeClasspath
- testImplementation
- testImplementationDependenciesMetadata

試しに以下のようにしてテスト以外のコンフィギュレーションだけチェックしてみると、grepで`myTestLibrary`は検出されません。

```console
$ ./gradlew app:dependencies --configuration debugRuntimeClasspath > dep1.txt
```

意図通り、テストのビルドにのみ参照されていることが確認出来ました。

# Multidex対応

自分のローカル環境では問題なかったのですが、GitHub Actionsでテストを動かしたときに、以下のようなエラーでInstrumentationテストのみ、ビルドに失敗していました。

```console
> Task :myTestLibrary:mergeExtDexDebugAndroidTest
D8: Cannot fit requested classes in a single dex file (# methods: 9xxxx ＞ 65536 ；
```

いわゆるメソッド数の65k問題が起きたようなのですが、アプリにはMultidex指定が既にしてあります。assembleコマンドは普通に成功していて、UnitTestも問題なく、Instrumentationテストだけで、です。

色々調べて、結局、**ライブラリ側にもmultidex指定が出来る(のでは)**という情報に行き当たりました。

参考になったのは以下のページの情報です。

https://living-sun.com/ja/android/27642-multidex-in-an-android-library-project-android-android-library-multidex.html

回答が機械翻訳なのか日本語が分かりづらいですが、[ドイツ語などで表示して](https://living-sun.com/en/android/27642-multidex-in-an-android-library-project-android-android-library-multidex.html)それをChromeのGoogle翻訳機能で表示してみると、こんな文章になっています。

> 私の仮説は、AARは.classファイルであり、通常のAndroidビルドプロセスにはライブラリ（AARとJAR）を含むすべてのプロジェクトファイルが含まれているというものです。ライブラリへのMultidexは、ライブラリがホームプロジェクトで作成されたときに、ライブラリが適切にデックスされていることを確認するのに役立ちます。

それで、`myTestLibrary`にもmultidexの指定を入れてみました。

```myTestLibrary/build.gradle
// ..(省略)

android {
// ..(省略)

    defaultConfig {
// ..(省略)

        multiDexEnabled true
    }

// ..(省略)
}

dependencies {
// ..(省略)

    // multidex
    implementation 'androidx.multidex:multidex:2.0.1'
}
```

これで、GitHub ActionsでもInstrumentationテストも無事ビルドでき、実行も出来るようになりました。(そろそろテスト実行時間が制限に指定している30分を超えそう:sweat:)

ローカル環境との違いはよく分からないですね・・・
GitHub ActionsでInstrumentationテストするために[android-emulator-runner](https://github.com/ReactiveCircus/android-emulator-runner)を利用させて貰っていますが、それが何か影響しているのかも知れません。

それと、私のローカル環境はmacOSが**Catalina**で、恐らくGitHub Actionsは`macOS-latest`としているので、**Big Sur**かなあ、てとこでしょうか？でもそれが64k問題になぜ絡むのかは・・・よく分かりません:sweat:

# まとめ
テスト用ライブラリモジュールを作るには、以下の手順で行う

(1) 通常通りライブラリモジュールを作成する
(2) テストで依存するライブラリを、`api`で参照する
(3) ライブラリを使う側のモジュールで、それぞれ`testImplementation`と`androidTestImplementation`にて作成したライブラリを依存設定する

今回の対応をしたプロジェクトは以下にありますので、参考にして下さい。
https://github.com/le-kamba/qiita_pedometer/tree/feature/lib_for_test

# 感想とか

ひとまず、テスト用にライブラリモジュールを作って参照させることが出来ました。
`api`や`implementation`を使っているのが果たして正しいのかどうか分かりませんが、いったんは希望する動作が出来たので、良しとします。(もしかしたら、テストでのmultidexの問題はこれのせいなのかも知れません。ローカルでは起きない理由は分かりませんが)

本当は、Koinを使っていてそのモッククラスやモックモジュールも全部移植したかったのですが、そうするとモックしたいクラスの元のインターフェースを参照しなければならず、そのままだとアプリモジュールをテスト用ライブラリが参照することになって循環参照してしまう・・・
ということでやめました。

この辺は、テスト用コードの共有化で対応できそうなので、時間を見てやってみようと思います。

まあ、なんでこんな面倒なことになっているかといえば、そもそもRobolectricは便利だけど完全にInstrumentationTestとは同等ではなくて、いったんInstrumentation版を書いてからRobolectricに移植していたりする必要があるからで、もっとはやくRobolectricが完全互換になればいいのにというお話です(違う)



# 余談

ところで・・・

jcenterでググると、閉鎖するとの情報があるのですが!?

- https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/
- https://qiita.com/takke/items/67a7f21cdcec0f13d7c8
- https://developers.karte.io/docs/app-faq-sunset-jcenter

Androidどうすんの？とおもって公式見たら・・・

https://developer.android.com/studio/build/jcenter-migration?hl=ja

追加情報全然きてないやん😭
Googleさぁぁん

まあ、完全シャットダウンは2022/2/1だからまだ余裕があると思っているのか・・・

AGP(Android Gradle Plugin)がjcenterでしか公開していないライブラリに依存してるらしいので、現時点ではすべてmavenCentralに移行することは不可能なようです。

参考)
https://lab.mo-t.com/blog/android-remove-jcenter


今年の年末辺りまでには、Goolgeから正式な対応アナウンスが出るのを期待してひとまず待つことにします。