Android フレームワーク依存のコードでユニットテストを行う方法


Android におけるテストの種類

Android には大きく分けて2種類のテストが存在します。

  • ローカル単体テスト(ユニットテスト)
    • 実機やエミュレーターを使わず、Android Studio で実行できるテスト
    • Android フレームワークに依存するコードが含まれない場合は基本的にこちらを選択する
  • インストルメンテーションテスト
    • 実機やエミュレーターを使って行うテスト
    • Android フレームワークに依存するコードもテストできる
    • 本番に近い環境のテストなので信頼性は高いが、手間もかかる

下の画像は UnitTestSample プロジェクトを新規作成したときのディレクトリ構造です。com.example.unittestsample (androidTest) にはインストルメンテーションテスト、com.example.unittestsample (test) にはユニットテストのファイルを保存するようになっています。

Gladle ファイルに依存関係を追記するときもテストタイプが関わってくるので注意が必要です。ライブラリを androidTestImplementation で記述してしまうと、ユニットテスト環境では使えません(私はこれでハマりました)。

build.gradle
dependencies {
    ...
    // すべて環境で使用可能
    implementation '...'
    // ユニットテストの環境でのみ使用可能
    testImplementation '...'
    // インストルメンテーションテストの環境でのみ使用可能
    androidTestImplementation '...'
    ...
}

ユニットテストで Android フレームワーク依存のコードをテストする

初期状態では Android フレームワーク依存のコードをユニットテストで動かすことはできません(例えば Context を使う関数のテストなど)。これを機能させるために Robolectric を利用します。Robolectric を使うことで JVM 上に Android 環境をシミュレートできるようになるので、テストのためにビルドしたり端末やエミュレーターを起動する必要がなくなります。

さっそく Robolectric のライブラリを追加します。

build.gradle
dependencies {
    ...
    testImplementation "org.robolectric:robolectric:4.5.1"
}

testOptions にも次の記述を追加します。

build.gradle
android {
    ...
    testOptions {
        unitTests.includeAndroidResources = true
    }
}

これで Robolectric が使えるようになりました。
プロジェクトを作成したときサンプルとして作られる ExampleUnitTest にテストを書いてみます。

ExampleUnitTest.kt
@Config(sdk = [28]) // エラー回避のため
@RunWith(RobolectricTestRunner::class) // Robolectric で実行
class ExampleUnitTest {
    @Test
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }

    // 以下のテストを追加
    @Test
    fun appName_isCorrect() {
        val context = InstrumentationRegistry.getInstrumentation().context
        val appName = context.getString(R.string.app_name)
        assertEquals(appName, "UnitTestSample")
    }
}

この状態で appName_isCorrect の左側にある ▶︎ から Run 'appName_isCorrect()' を選択してテスト実行します。Robolectric のおかげで Context が機能して appName が取得できるようになり、テストが成功するかと思います。

1行目に @Config(sdk = [28]) を追記していますが、これが無いと以下のエラーが発生したため、Android SDK のバージョンを指定しています。

ava.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 30 requires Java 9 (have Java 8)

AndroidJUnit4 でテストをより快適にする

先ほどはテストランナーに RobolectricTestRunner を指定しましたが、AndroidJUnit4 を使うと、テストのタイプによって自動的にテストランナーを切り替えてくれるようになります。インストルメンテーションテストの場合は AndroidJUnit4 ランナー(Android 環境の標準ランナー)を使用し、ユニットテストの場合は RobolectricTestRunner を使用します。

また、Context などの Android フレームワークへのアクセス方法もインストルメンテーションテストとユニットテストで共通なので、AndroidJUnit4 を使えばテストのタイプを意識することなく同じ内容のテストが書けるようになります。

それでは AndroidJUnit4 を使うために以下のライブラリを追加します。

build.gradle
testImplementation 'androidx.test.ext:junit:1.1.2'

そして AndroidJUnit4 をテストランナーに指定します。

ExampleUnitTest.kt
@Config(sdk = [28])
@RunWith(AndroidJUnit4::class)
class ExampleUnitTest {
    ...
}

もう一度テストを実行すると、問題なくテストが通るのを確認できるかと思います。

まとめ

  • Android にはユニットテストとインストルメンテーションテストの2種類のテストがある
  • ユニットテストで Android フレームワークのコードを書きたい場合は Robolectric を使おう
  • AndroidJUnit4 を使うと2種類のテストを意識することなくテストが書けるのでより快適!