概要
Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(16)ライブラリバージョンアップ2021春(2)
上記の記事にて、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系処理をライブラリモジュール化する
- 本体アプリモジュールのテスト用の依存関係をスッキリさせる
テスト関連の依存ライブラリ
元のアプリのコードは以下にあります。
アプリモジュールのbuild.gradle
では、これだけのテスト関係の依存ライブラリがありました。
EspressoとRobolectricとMockitoが大きな依存ライブラリになっていますが、Androidのテスト用パッケージも細々としたものがあって、testImplementation
とandroidTestImplementation
の双方に必要なために重複して宣言せねばならず長くなっています。
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とします。
Android Libraryを選んで、[Next]をクリック
任意のモジュール名を入力して、[Finish]をクリック
- 必要があれば、[Edit]をクリックしてパッケージ名を変更
モジュールが追加されます。(少し時間がかかるかも)
ここまでは、普通のライブラリモジュールを作るのと変わりありません。
まあ、ぶっちゃけこれ以降もそうなんですが。
2. dependenciesの設定
テストライブラリモジュールの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)向けの関数を移植(ファイルをコピー)しました。
それぞれのファイルの中身は、まとめに記載してあるリポジトリをご参照下さい。
アプリ側の変更
1. dependencies設定の変更
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での実行も考慮してコマンドラインからも実行しておきましょう。
(どちらかでだけビルドエラーになったりする場合があります)
# 単体テスト
$ ./gradlew app:testDebugUnitTest
4.2 Instrumentationテスト
同様に、Android Studioからだけでなく、コマンドラインからも実行してみます。
実行する前には、実機を繋いでおくかエミュレーターを起動しておくのをお忘れなく。
$ ./gradlew app:connectedCheck
実は、ここでビルドエラーが起きました。
D8: com.android.tools.r8.a: MethodHandle.invoke and MethodHandle.invokeExact are only supported starting with Android O (--min-api 26)
エラーメッセージでググって、以下に辿り着きました。
Mockitoが依存しているobjenesisというライブラリとのバージョンコンフリクトのようです。
最後の方までコメントを頑張って読んでいくと、3.9.0で直されたようなので、mavenリポジトリ等を確認してみると、公開済みになっているようなので、(build.gradleで参照しているのはjcenterですが)、さっそくバージョンを上げてみます。
// Mockito
api 'org.mockito:mockito-core:3.9.0'
api 'org.mockito:mockito-inline:3.9.0'
これでテストをビルド、実行してみると、エラーが起きず無事テストが実行されました。
5. 依存関係を確認してみる
myTestLibraryが、テスト以外のビルドで使われていないことをちょっと確認してみようとおもいます。
Gradleのdependencies
コマンドで依存関係を吐き出してくれますが、そのままだとターミナルにずらずらっと吐かれて見づらいので、たいていファイルにパイプして見ています。
$ ./gradlew app:dependencies > deps.txt
このとき作成したdeps.txt
をうっかりコミットしてしまうことが多いのでお気を付けてw
deps.txt
内をmyTestLibrary
でgrep検索を掛けてみます。
以下のキャプチャーはMacでは定番の(?)テキストエディタCotEditorでのgrep結果です。
検出された箇所を1箇所ずつチェックしましたが、以下のコンフィギュレーション下にしか検出されていませんでした。
- androidTestImplementation
- androidTestImplementationDependenciesMetadata
- debugAndroidTestCompileClasspath
- debugAndroidTestImplementationDependenciesMetadata
- debugAndroidTestRuntimeClasspath
- debugUnitTestCompileClasspath
- debugUnitTestImplementationDependenciesMetadata
- debugUnitTestRuntimeClasspath
- releaseUnitTestCompileClasspath
- releaseUnitTestImplementationDependenciesMetadata
- releaseUnitTestRuntimeClasspath
- testImplementation
- testImplementationDependenciesMetadata
試しに以下のようにしてテスト以外のコンフィギュレーションだけチェックしてみると、grepでmyTestLibrary
は検出されません。
$ ./gradlew app:dependencies --configuration debugRuntimeClasspath > dep1.txt
意図通り、テストのビルドにのみ参照されていることが確認出来ました。
Multidex対応
自分のローカル環境では問題なかったのですが、GitHub Actionsでテストを動かしたときに、以下のようなエラーでInstrumentationテストのみ、ビルドに失敗していました。
> Task :myTestLibrary:mergeExtDexDebugAndroidTest
D8: Cannot fit requested classes in a single dex file (# methods: 9xxxx > 65536 ;
いわゆるメソッド数の65k問題が起きたようなのですが、アプリにはMultidex指定が既にしてあります。assembleコマンドは普通に成功していて、UnitTestも問題なく、Instrumentationテストだけで、です。
色々調べて、結局、**ライブラリ側にもmultidex指定が出来る(のでは)**という情報に行き当たりました。
参考になったのは以下のページの情報です。
回答が機械翻訳なのか日本語が分かりづらいですが、ドイツ語などで表示してそれをChromeのGoogle翻訳機能で表示してみると、こんな文章になっています。
私の仮説は、AARは.classファイルであり、通常のAndroidビルドプロセスにはライブラリ(AARとJAR)を含むすべてのプロジェクトファイルが含まれているというものです。ライブラリへのMultidexは、ライブラリがホームプロジェクトで作成されたときに、ライブラリが適切にデックスされていることを確認するのに役立ちます。
それで、myTestLibrary
にもmultidexの指定を入れてみました。
// ..(省略)
android {
// ..(省略)
defaultConfig {
// ..(省略)
multiDexEnabled true
}
// ..(省略)
}
dependencies {
// ..(省略)
// multidex
implementation 'androidx.multidex:multidex:2.0.1'
}
これで、GitHub ActionsでもInstrumentationテストも無事ビルドでき、実行も出来るようになりました。(そろそろテスト実行時間が制限に指定している30分を超えそう)
ローカル環境との違いはよく分からないですね・・・
GitHub ActionsでInstrumentationテストするためにandroid-emulator-runnerを利用させて貰っていますが、それが何か影響しているのかも知れません。
それと、私のローカル環境はmacOSがCatalinaで、恐らくGitHub ActionsはmacOS-latest
としているので、Big Surかなあ、てとこでしょうか?でもそれが64k問題になぜ絡むのかは・・・よく分かりません
まとめ
テスト用ライブラリモジュールを作るには、以下の手順で行う
(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どうすんの?とおもって公式見たら・・・
追加情報全然きてないやん😭
Googleさぁぁん
まあ、完全シャットダウンは2022/2/1だからまだ余裕があると思っているのか・・・
AGP(Android Gradle Plugin)がjcenterでしか公開していないライブラリに依存してるらしいので、現時点ではすべてmavenCentralに移行することは不可能なようです。
参考)
https://lab.mo-t.com/blog/android-remove-jcenter
今年の年末辺りまでには、Goolgeから正式な対応アナウンスが出るのを期待してひとまず待つことにします。