はじめに
コンポーザブルのテストを行う際に、AndroidTestディレクトリを使ってテストを行うことが多いと思います。その際に、毎回エミュレーターを立ち上げて実行するため一回のテストにかかる時間は通常のユニットテストと比べて多くかかります。
そこで、エミュレーターを使わずにコンポーザブルに対するテストを行うために、Robolectric
を使ってユニットテストとしてテストを行ってみようと思います。
この方法は、Droid Kaigi 2022のアプリでも採用されています。
TL;DR
結果として、Robolectricを使ってunitTestでコンポーザブルをテストするようにした結果、大幅な時間の短縮につながりました。
テストの種類 | 時間 |
---|---|
unitTest | 3 sec 411 ms |
androidTest | 46 sec |
依存関係の追加やテストのための設定
一番大事なのが、依存関係の設定です。
testImplementation
や、androidTestImplementation
を正しく使用しないとエラーが発生してテストが正しく実行されない可能性がありますのでご注意ください。
モジュールレベルのbuild.gradleの設定
unitTestとandroidTestで必要な設定と依存関係のみにしぼってbuild.gradleの内容をまとめました。
これらを参考に、自身のプロジェクトに依存関係を追加してみてください。
plugins {
// Kapt
id 'kotlin-kapt'
// Hilt
id 'dagger.hilt.android.plugin'
}
android {
defaultConfig {
testInstrumentationRunner "com.takagimeow.myapplication.core.testing.TestRunner"
}
kotlinOptions {
freeCompilerArgs = ["-Xcontext-receivers"]
}
testOptions {
unitTests.includeAndroidResources = true
}
}
dependencies {
// Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
// Test
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
testImplementation "io.mockk:mockk:1.12.4"
testImplementation "org.robolectric:robolectric:4.9"
// Hilt for Test
testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
testImplementation 'androidx.compose.ui:ui-test-junit4'
kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"
// AndroidTest
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
androidTestImplementation "com.google.dagger:dagger:$hilt_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
kaptAndroidTest "com.google.dagger:dagger-compiler:$hilt_version"
}
testOptions
ブロックで、unitTests.includeAndroidResources = true
を設定しています。
もし、この設定を行わない場合は次のエラーが発生します。
Unable to resolve activity for Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=org.robolectric.default/androidx.activity.ComponentActivity } -- see https://github.com/robolectric/robolectric/pull/4736 for details
ルートレベルのbuild.gradleの設定
このbuild.gradleでは、共通のhilt_version
の定義などを行います。加えてテストの実行に必要な依存関係も追加しています。
buildscript {
ext {
hilt_version = '2.44.2'
}
dependencies {
classpath("com.google.dagger:hilt-android-gradle-plugin:$hilt_version")
// Test
classpath("junit:junit:4.13.2")
}
}
テストに必要なファイルを作成する
画像に表示されているファイルには作成する必要のないファイルも含まれています。
今回は、Droid Kaigi2022に則って、RobotTestRule
というカスタムルールを実装してコンポーザブルをunitTestディレクトリ内でテストしたいため、下記の4つのファイルを作成しています。
testingパッケージ
testing
パッケージには、各テストにて共通で使えるカスタムルールに必要なファイルを作成します。
HiltAndroidAUtoInjectRule
詳しい実装内容については、こちらを参照してください。
package com.takagimeow.myapplication.core.testing
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class HiltAndroidAutoInjectRule(
private val testInstance: Any
) : TestRule {
override fun apply(base: Statement?, description: Description?): Statement {
println(testInstance::class)
val hiltAndroidRule = HiltAndroidRule(testInstance)
return RuleChain
.outerRule(hiltAndroidRule)
.around(HiltInjectRule(hiltAndroidRule))
.apply(base, description)
}
}
HiltInjectRule
詳しい実装については、こちらを参照してください。
package com.takagimeow.myapplication.core.testing
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.rules.TestWatcher
import org.junit.runner.Description
class HiltInjectRule(private val rule: HiltAndroidRule) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
rule.inject()
}
}
HiltTestActivity
詳しい実装については、こちらを参照してください。
package com.takagimeow.myapplication.core.testing
import androidx.activity.ComponentActivity
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HiltTestActivity : ComponentActivity()
このHiltTestActivity
をテスト時に使用するとエラーが発生することがあります。その解決方法を次の章にて解説しています。
RobotTestRule
詳しい実装については、こちらを参照してください。
package com.takagimeow.myapplication.core.testing
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class RobotTestRule(
private val testInstance: Any
) : TestRule {
val composeTestRule = createAndroidComposeRule<HiltTestActivity>()
override fun apply(base: Statement?, description: Description?): Statement {
return RuleChain
.outerRule(HiltAndroidAutoInjectRule(testInstance))
.around(composeTestRule)
.apply(base, description)
}
}
この設定に辿り着くまでに、いろいろ試行錯誤している最中は、.outerRule(HiltAndroidAutoInjectRule(testInstance))
の呼び出し部分で何度もエラーが発生してしまいました。
java.lang.IllegalStateException: Hilt test, com.takagimeow.myapplication.feature.addmember.AddMemberScreenTest, cannot use a @HiltAndroidApp application but found com.takagimeow.myapplication.MyApplication. To fix, configure the test to use HiltTestApplication or a custom Hilt test application generated with @CustomTestApplication.
createAndroidComposeRule()
を呼び出す際に、ComponentActivity
ではなく、独自に作成したHiltTestActivity
を指定すると、次のエラーが発生しました。
Unable to resolve activity for Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.takagimeow.myapplication.debug/com.takagimeow.myapplication.core.testing.HiltTestActivity } -- see
これを解決するためには、testディレクトリに新しくAndroidManifest.xmlを配置するのではなく、debugディレクトリにAndroidManifest.xmlを配置します。
testディレクトリにAndroidManifest.xmlを配置して、HiltTestActivityを登録したとしてもテスト時に認識されないため上記のエラーは解決しません
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name=".core.testing.HiltTestActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MyApplication"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
テスト時は、debug
ビルドタイプが適用されます。
そのため、debugディレクトリ内のAndroidManifest.xmlにHiltTestActivitiy
を登録することで、テスト時に認識させることができるようになります。
もう一つ注意点として、build.gradleにて次の設定がされていることを必ず確認してください。
android {
testOptions {
unitTests.includeAndroidResources = true
}
}
TestRunner
このテストランナーは、androidTestにて実行されるインストゥルーメンテーションテストを行う際にHiltテストアプリを使う場合にのみ作成が必要なものです。
unitTestにおけるテストを行う際には作成する必要は特にありませんので、このままスルーしてもかまいません。
package com.takagimeow.myapplication.core.testing
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class TestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
ちなみに、作成したテストランナーはモジュールレベルのbuild.gradleにて次のように設定します。
defaultConfig {
testInstrumentationRunner "com.takagimeow.myapplication.core.testing.TestRunner"
}
カスタムテストランナーを作成しない場合は、androidx.test.runner.AndroidJUnitRunner
を設定します。
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
実際のテストに必要なファイル
XxxScreenRobot
詳しい実装については、こちらを参照してください。
package com.takagimeow.myapplication.feature.addmember
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.test.onNodeWithText
import com.takagimeow.myapplication.core.testing.RobotTestRule
import com.takagimeow.myapplication.R
import javax.inject.Inject
class AddMemberScreenRobot @Inject constructor() {
context(RobotTestRule)
fun checkAddMemberVisible() {
composeTestRule.onNodeWithText(composeTestRule.activity.getString(R.string.title_add_member)).assertExists()
}
@OptIn(ExperimentalMaterial3Api::class)
operator fun invoke(
robotTestRule: RobotTestRule,
function: context(RobotTestRule) AddMemberScreenRobot.() -> Unit
) {
robotTestRule.composeTestRule.setContent {
// これが実際にテスト対象のコンポーザブル
AddMemberScreen()
}
function(robotTestRule, this@AddMemberScreenRobot)
}
}
XxxScreenTest
詳しい実装については、こちらを参照してください。
package com.takagimeow.myapplication.feature.addmember
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.takagimeow.myapplication.core.testing.RobotTestRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
// @Config(application = HiltTestApplication::class)
class AddMemberScreenTest {
@get:Rule val robotTestRule = RobotTestRule(this)
@Inject lateinit var addMemberScreenRobot: AddMemberScreenRobot
@Test
fun smoke_test() {
addMemberScreenRobot(robotTestRule) {
checkAddMemberVisible()
}
}
}
Robolectricの設定
Robolectricを使用してコンポーザブルをテストするため、使用するアプリケーションをrobolectric.propertiesファイルを使って設定する必要があります。
ここでは、dagger.hilt.android.testing.HiltTestApplication
を指定します。
application=dagger.hilt.android.testing.HiltTestApplication
# https://github.com/robolectric/robolectric/issues/6593
instrumentedPackages=androidx.loader.content
テストごとに個別に指定することも可能です。
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
@Config(application = HiltTestApplication::class)
class AddMemberScreenTest {
...
}
もし、@HiltAndroidTest
を使っているにも関わらず、上記の設定を怠った場合は次のエラーが発生します。
java.lang.IllegalStateException: Hilt test, com.takagimeow.myapplication.feature.addmember.AddMemberScreenTest, cannot use a @HiltAndroidApp application but found com.takagimeow.myapplication.MyApplication. To fix, configure the test to use HiltTestApplication or a custom Hilt test application generated with @CustomTestApplication.
まとめ
同じテストの内容をandroidTest
とunitTest
で実行してみた結果、次のような結果になりました。
テストの種類 | 時間 |
---|---|
unitTest | 3 sec 411 ms |
androidTest | 46 sec |
これだけの差が生まれますので、少々設定は面倒ですが、Robolectrictを導入してコンポーザブルをunitTest内でテストをできるようにして良かったと感じています。
参考にした記事