Android コンポーネント(Activity)の単体テスト
最近はスキレットで肉を焼くのに地味にハマっています。
さて今回は、Android のテストについて書きます。一般的なテストについてはここでは解説しません。
テストのピラミッド
テストは大きく分けて3つの層に分かれます。ピラミッドで表しているのをよく見ます。
ピラミッドなので、一番下が太く、一番上が細い三角形△になっています。 太さはそのまま全体の中で占める割合を表しています。上から順番に並べると
- 大規模テスト(UI テスト(E2Eテスト))
- 中規模テスト(Integration(結合) テスト)
- 小規模テスト(Unit(単体) テスト)
の順番です。一般的には、上に行くほど本番運用の再現性が高くなりますが、テストを開始してから完了するまでの時間(実行時間)や保守およびデバッグの労力が増えます。 先に挙げたURLのドキュメントを読むと以下のような割合が推奨されています。
一般的には、小規模テスト 70%、中規模テスト 20%、大規模テスト 10% の割合でテストを作成することをおすすめします。
今回作成するテストは、小規模テストにあたる単体テストです。
Android のテスト概要
Android のテストの実行方法は大きく分けて2種類あります。
- 実機、エミュレータで実行するテスト
- ローカルマシン内で実行するテスト
これは、先に挙げたピラミッドのどれかに当てはまるわけではありません。各層でどちらのテストも行うことが出来ます。
実機、エミュレータで実行するテスト
実際の使用に近いテストですので、本番運用の再現度が高いです。ただし、実行するまでの時間、テスト実行から完了までの時間が長くなります。
エミュレータは、Android Studio 標準で備えてある Android Emulator を使うのが一般的でしょう。
テストコードは、app/src/androidTest
に配置されます。
インストゥルメントテスト、インストゥルメント化テストとも呼ばれ、意味的には実機テストのことだと思います。といっても実機に限らずエミュレータでも行えるので実機に近いテストといった感じでしょうか。
ローカルマシン内で実行するテスト
ローカルマシン、つまり開発しているPC内で行うテストです。テスト対象がAndroid に依存しないコードであれば、一般的な Java, Kotlin のコードをテストするように行えますが、Android フレームワークに依存する場合は、Robolectric のようなフレームワーク(テストフレームワーク?)を利用して行う必要があります。
Robolectric というのは、先と似た話になりますが、Android フレームワークに依存したコードを JVM 上で実行するためのフレームワークです。そのため、実機やエミュレータのテストと比べると再現性は落ちてしまいますが、実行速度は Android に依存しないテストに近い速度で実行できます。
テストコードは app/src/test
に配置されます。
また、Robolectric はテストを行うための API(ライブラリ?)を提供していますが、ここでは Robolectric は実行環境としてだけ使用し、テストコードは、AndroidX Test API を使用します。テストコードを AndroidX Test API に統一すると一つのソースコードで単体テストと実機テストの両方でテストできるようになります。ここでは紹介しません(そのやり方を知らないので)。
参考
Espresso とは
Espresso とは、Android UI テストのためのライブラリです。AndroidX に含まれています。Espresso を使うことで簡潔に書くことができます。同期的に処理が行われるので、信頼性の高いテスト結果が得られます
Espresso は4つの主なコンポーネントがあります。
- Espresso
- ViewMatchers
- ViewActions
- ViewAssertions
Espresso
onView
や onData
メソッドを使ってビューとやり取りするためのエントリポイントです。
ViewMatchers
Matcher<? super View>
インタフェースを実装するオブジェクトのコンポーネントです。onView
メソッドにこのコンポーネントを一つ以上渡して現在のビュー階層からビューを特定します。一意に特定できない場合は、例外が投げられます。
ViewActions
ViewInteraction.perform
メソッドに渡すことができる ViewAction
オブジェクトのコレクションです。
ViewAssertions
ViewInteraction.check
メソッドに渡すことができる ViewAssertion
オブジェクトのコレクションです。アサーションが失敗した場合、例外が投げられます。
Hamcrest
Espresso のコンポーネントではないですが、Espresso を使うときによく一緒に使われます。マッチャーオブジェクトを記述するためのフレームワークです。
例
Espresso を使った UI のテストコードは以下のようになります。
Espresso.onView(ViewMatchers.withId(R.id.helloWorld))
.check(ViewAssertions.matches(ViewMatchers.withText("Hello World!")))
ViewMatchers.withId()
でビューを特定する条件を指定し、onView()
に渡します。次に check()
の引数に指定した条件が指定のビューに一致するか検証します。ここでは、特定したビューが "Hello World!"という文字列を持っているか検証しています。
実践
ここからは、プロジェクトを作成し、実際にテストコードを書いてみます。
Android Studio のバージョンは、3.6.3 です。
プロジェクトの作成
次のような設定をして、プロジェクトを作成します。
- Project Template
- EmptyActivity
- Configure
環境
今回実行する環境です。
- Android Gradle Plugin Version
- 3.6.3
- Gradle Version
- 5.6.4
- Kotlin Version
- 1.3.71
- Compile Sdk Version
- 29
- BuildT Tools Version
- 29.0.0
プロジェクトを作成したら、 app/src/test/ExampleUnitTest.kt
を開き、テストを実行します。ソースコード内の行数の右隣に緑の三角が横に向いたようなアイコンがあると思います。それをクリックすることで実行できます。
テストの作成
先に実行したテストは、プロジェクトの内容と関係ないテストなので、MainActivity
をテストするためのファイルを作成します。
テスト対象のファイルを開き(MainActivity.kt)、開いたら Ctrl+Shift+T
(Windowsの場合)を押します。すると図のようなメニューが開くので、Create New Test
をクリックします。
Testing library
を JUnit4
にします。
OK をクリックするとChoose Destination Directory
ウィンドウが開くので、/app/src/test/...
の方を選択します。OK をクリックすると指定したディレクトリにテストファイルが作成されます。作成されたMainActivityTest.kt
ファイルの中身は以下のようになっていました。
package com.example.ochadukebiyori.componenttestexample2 import org.junit.Assert.* class MainActivityTest
テストの追加
MainActivity のレイアウトファイルを見ると画面の真ん中に "Hello World!" という文字が表示されているので、これをテストしたいと思います。レイアウトファイルを開いたついでに、"Hello World!" を表示しているTextView
に helloWorld
という id を指定します。
テストは、@Test
が付与されたメソッドがテストになります。MainActivityTest.kt
を以下のようにします。
package ... import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MainActivityTest { @Test fun 起動したらHelloWorldが表示されるべき() { onView(withId(R.id.helloWorld)) .check(matches(withText("Hello World!"))) } }
@RunWith
でテストランナーを指定します。AndroidJUnit4
はAndroid の標準的なテストランナーです。テストのメソッド名はわかりやすさを重視するため日本語で書いています。テスト内容は、テスト対象のビューを特定する id(helloWorld
)がテキストに "Hello World!" という文字列を持っているか検証しています。
Espresso や Robolectric のようなライブラリは、デフォルトでは使えないので、build.gradle
を編集し、モジュールを追加します。
dependencies { ... testImplementation 'androidx.test.espresso:espresso-core:3.2.0' testImplementation 'androidx.test.ext:junit-ktx:1.1.1' testImplementation 'org.robolectric:robolectric:4.3.1' ... }
実行すると次のようなエラーが出ると思います(ワーニングも出ますがそちらは無視します)。
java.lang.RuntimeException: No activities found. Did you forget to launch the activity by calling getActivity() or startActivitySync or similar?
これは、Activity が作成されていないのが原因です。次のコードを追加することでアクティビティが作成されます。
import androidx.test.core.app.launchActivity ... class MainActivityTest { val scenario = launchActivity<MainActivity>() ... }
launchActivity()
を使うためには、モジュールを追加する必要があるので、build.gradle
に次のように追加します。
dependencies { ... testImplementation 'androidx.test:core-ktx:1.2.0' ... }
ActivityScenario
は、アクティビティのライフサイクルのための API を提供してくれるモジュールです。
実行すると次のようなエラーが出るかも知れません(あるいは、実行する前に赤い波線で示されているかも)
Cannot inline bytecode built with JVM target 1.7 into bytecode that is being built with JVM target 1.6
これは、launchActivity()
が JDK1.7
以上じゃないと使えないためです。次のように build.gradle にターゲットを指定します。
android { kotlinOptions { jvmTarget = '1.8' } }
ついでに Robolectric でリソースを使えるようにするために以下も行います。
リソースを使用する場合は、次のように includeAndroidResources
オプションを有効にします。基本的にビューの特定は id で行うため、必須になります。しかし、Android Studio 3.4 以上はデフォルトで有効になっている?ような記述が見られるので必要ないかもしれません。ですが、自分の環境では指定していない場合、エラーが出たので追加しています。
android { ... testOptions { unitTests { includeAndroidResources = true } } }
gradle を同期して、実行すると次のようなエラーが出るかもしれません。
java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8)
これは、自分はあまり理解出来ていないのですが、 Robolectric は、ターゲットバージョンに合わせて環境が作られます。今回だと Android SDK 29
です。Robolectric を 29 で動かす場合は、 JDK が 9 以上である必要があるそうです。ですが、今は JDK 8 で動かしているため上記のようなエラーが出ます。
ここでは、Robolectric でのターゲットバージョンを 28 にすることで回避します。テストの再現性が落ちるので良くないですが、練習のため簡単な方法で回避します(本当は JDK 9 以上にするのが良いと思います)。
app/src/test/
に resources
ディレクトリを追加し、そのディレクトリ内に robolectric.properties
ファイルを追加します。そして、以下を追記します。
sdk=28
再度実行するとテストが通るはずです。
ActivityScenario
はデバイスの状態を自動でクリーンアップ(初期化)しないため、テスト終了後もアクティビティが実行され続ける可能性があります。ですので、テストの最後には次のように明示的にクローズします。
... @Test fun 起動したらHelloWorldが表示されるべき() { onView(withId(R.id.helloWorld)) .check(matches(withText("Hello World!"))) scenario.close() } ...
再度テストを実行し、テストが通ることを確認します。
テストコードのリファクタリング
今回の目的は達成しましたが、途中で出てきた ActivityScenario は閉じることを忘れるとテストコードに予期せぬバグが発生する可能性があるため、それを避けるように修正します。
ActivityScenario は、テスト終了後もアクティビティを実行し続ける可能性があると書きました。このActivityScenario を管理してくれるクラスがいます。それは ActivityScenarioRule
です(昔は、ActivityTestRule
でしたが、今は非推奨になっています)。
このクラスを次のように書いておくと、テストの開始前と終了後にアクティビティの作成と開放を行ってくれます。
... import org.junit.Rule import androidx.test.ext.junit.rules.activityScenarioRule ... class MainActivityTest { @get:Rule var rule= activityScenarioRule<MainActivity>() ... }
先程追加した ActivityScenario の処理は不要になったので、削除しています。
テストを実行して、通るか確認しましょう。
ActivityScenarioRule は、ActivityScenario を持っているので次のように参照する事ができます。
val scenario = rule.scenario
アクティビティは自動で終了してくれますが、手動でアクティビティを終了しても問題ありません。
お疲れさまでした。以上です。
ここまでの状態を Github の方に上げています。ブランチは ActivityUnitTestExample_1
です。
続きを書きました。
pickles-ochazuke.hatenablog.com
雑記
FGO アニメ見終わりました。マーリンって名が出るたびになんか聞き覚えあるなぁと思っていたのですが、ドラクエ5のまほうつかいが仲間になったときの名前でした。通りで魔法使いっぽい名前してるなと思いました。