LoginSignup
6
9

More than 1 year has passed since last update.

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

Posted at

概要

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のテスト用パッケージも細々としたものがあって、testImplementationandroidTestImplementationの双方に必要なために重複して宣言せねばならず長くなっています。

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

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

select_android_library.png

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

input_library_name.png

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

library_add.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'
}

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

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

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

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

lib_files.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での実行も考慮してコマンドラインからも実行しておきましょう。
(どちらかでだけビルドエラーになったりする場合があります)

# 単体テスト
$ ./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ですが)、さっそくバージョンを上げてみます。

app/build.gradle
    // 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結果です。

coteditor_grep.png

検出された箇所を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の指定を入れてみました。

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を利用させて貰っていますが、それが何か影響しているのかも知れません。

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

まとめ

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

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

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

感想とか

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

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

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

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

余談

ところで・・・

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

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

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

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

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

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

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

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9