16
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

コンポーザブルのテストを爆速にする方法

Last updated at Posted at 2023-01-31

はじめに

コンポーザブルのテストを行う際に、AndroidTestディレクトリを使ってテストを行うことが多いと思います。その際に、毎回エミュレーターを立ち上げて実行するため一回のテストにかかる時間は通常のユニットテストと比べて多くかかります。

そこで、エミュレーターを使わずにコンポーザブルに対するテストを行うために、Robolectricを使ってユニットテストとしてテストを行ってみようと思います。

この方法は、Droid Kaigi 2022のアプリでも採用されています。

TL;DR

結果として、Robolectricを使ってunitTestでコンポーザブルをテストするようにした結果、大幅な時間の短縮につながりました。

テストの種類 時間
unitTest 3 sec 411 ms
androidTest 46 sec

依存関係の追加やテストのための設定

一番大事なのが、依存関係の設定です。
testImplementationや、androidTestImplementationを正しく使用しないとエラーが発生してテストが正しく実行されない可能性がありますのでご注意ください。

モジュールレベルのbuild.gradleの設定

unitTestandroidTestで必要な設定と依存関係のみにしぼって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")
    }
}

テストに必要なファイルを作成する

名称未設定のデザイン (40).png

画像に表示されているファイルには作成する必要のないファイルも含まれています。

今回は、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.

まとめ

同じテストの内容をandroidTestunitTestで実行してみた結果、次のような結果になりました。

テストの種類 時間
unitTest 3 sec 411 ms
androidTest 46 sec

これだけの差が生まれますので、少々設定は面倒ですが、Robolectrictを導入してコンポーザブルをunitTest内でテストをできるようにして良かったと感じています。

参考にした記事

16
17
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
16
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?