NTTドコモR&Dの技術ブログです。

Android(XR)のユニットテスト入門

※ 本記事は 2026/3/31 以前にNTTコノキューにて記載した記事になります

はじめに

この記事はNTTコノキュー アドベントカレンダー2025の記事です。

こんにちは、NTTコノキューの中矢です。業務ではUnityとC#、最近はKotlinと格闘しています。

本記事は、Androidのユニットテストの入門および紹介記事です。また、ユニットテストにはMockito(モキート)を利用しているため、Mockitoの使い方の雰囲気がわかるかなと思います。

Mockito:https://site.mockito.org/

特に最近はAndroid XRでKotlinを利用するため、XR業界でもKotlinでユニットテストを書く人口が増えると思われます。

この記事が少しでも、Androidのユニットテストに挑戦する人の参考になれば幸いです。

環境

  • MacBook Apple M4

  • Android Studio Narwhal 4 Feature Drop | 2025.1.4 Canary 2

ユニットテストの導入

とにもかくにもまずはユニットテストライブラリを導入します。build.gradle.kts (Module :app)のdependencies {} ブロック内に記載していきます。

KotlinのユニットテストフレームワークのJUnitは以下です。


testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.3.0")

また、Mockitoと呼ばれるテストデータを簡単に作れるライブラリも導入します。(内容は後述します)


testImplementation("org.mockito:mockito-core:5.12.0")
testImplementation("org.mockito.kotlin:mockito-kotlin:6.0.0")

テスト対象

本記事のユニットテストのテスト対象を説明します。せっかくなので、Android XR用のサンプルアプリとして作ったものをテストしますが、テスト方法自体は普通のAndroid(Jetpack Compose製)と同様です。

テスト対象は簡単に上記画面の左の赤枠に囲まれている"目次"のテキスト表示が合っているかどうかの確認です。ここは本来はAPIなどでサーバから取ってくることを想定していますが、今回は簡単のために文字情報をアプリに組み込んでいます。

具体的にテストするメソッドは、TrainingViewModelと呼ばれるViewModel内の以下です。処理内容は以下のコメント通りで、値を指定してRepositoryから文字情報を引っ張ってくるようなものです。


fun getSelectedContentTitle(contentState: TrainingContentState): String {
    // 実装は省略
    // TrainingContentStateクラスを引数に入れると"目次"情報を取得する
    // TrainingContentState(selectedIndex = 1, currentPageIndex = 0)
    // のように値を入れると、Stringで文字情報を取得できる
    // 取得する文字情報はTrainingContentRepositoryから取得する
}

上記メソッドで利用しているTrainingContentRepositoryはサーバと連携したりすることも可能です。そのため、ユニットテストではサーバと接続していなくてもテストできるようにしなければなりません。

また、アプリの初期化には並列処理を行っているため、そこも考慮しないといけません。

ユニットテストの準備

本記事ではユニットテストでいくつか準備しています。まず本記事で作成しているアプリは初期化で並列処理をしているため、テストで並列処理をできるように、TestDispatcherRule.ktを作成し、StandardTestTestDispatcherRuleクラスを記述します。


@ExperimentalCoroutinesApi
class StandardTestTestDispatcherRule(
    val testDispatcher: TestDispatcher = StandardTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

これをユニットテスト本体で設定することで、テスト中に非同期処理を制御することができます。

次にユニットテスト本体のファイルを以下のようにTrainingViewModelTest.ktとして作成します。

TrainingViewModelTest.ktファイルにTrainingContentDummyDataとしてダミーのテストデータを記述します。あとでMockitoでテストデータを引っ張ってくるための前準備です。


object TrainingContentDummyData {
    // 省略...
    private val dummyDescriptions = /*.省略 */

    // ダミーのTrainingMainContentインスタンス
    private val dummyMainContents = TrainingMainContent(
        title = "ダミーの学習タイトル",
        descriptions = dummyDescriptions
    )
    // 省略...
}

上記を外部から引っ張ってくれるテストデータとします。

ユニットテスト本体

ここから本番でTrainingViewModelTest.ktファイルにユニットテストを書いていきます。

まずTrainingViewModelTestクラスを書き、変数を定義していきます。


@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(MockitoJUnitRunner::class)
class TrainingViewModelTest {
    // テスト用のDispatcherルールを適用
    @get:Rule
    val testDispatcherRule = StandardTestTestDispatcherRule()

    // モックのリポジトリを作成
    @Mock
    lateinit var mockRepository: TrainingContentRepository

    private lateinit var viewModel: TrainingViewModel

    // 省略(後述)
}

まず並列処理をテストで制御できるようにTestDispatcherRuleを指定します。これには前準備で記述したStandardTestTestDispatcherRuleクラスを適用しています。

次にMockアノテーション(Mockito)でモックのリポジトリを作成しています。このようにモックのリポシトリを作成することで、実際にAPIなどと通信しなくてもテスト用にデータを用意することができます。

これをすることにより、実際にサーバと通信しないでユニットテストを回すことができるので、積極的にMockitoなどのライブラリを用いてモックを作成し、テストを作っていきたいものです。(アプリの実装でもRepositoryを活用したいですね。)

続いて、テスト前のセットアップを以下のように行います。


@Before
fun setUp() {
    // リポジトリ内部で並列処理をしているため、runBlockingを利用している
    whenever(runBlocking { mockRepository.getAllTrainingContents() }).thenReturn(
        listOf(
            dummyTrainingContentIndex,
            dummyTrainingContentIndex2
        )
    )
    viewModel = TrainingViewModel(mockRepository)
}

Beforeアノテーションでテスト前に実行するように指定します。

先程の前準備で用意したテストデータのTrainingContentDummyDataから、モックのリポジトリを作成し、そのモックのリポジトリを利用してViewModelのインスタンス作成します。これによりテストデータが格納されたViewModelを準備して、ViewModelのロジックのテストが可能となります。

最後の実際のテストは以下です。JUnit 4ではテストメソッド名をバッククォートで囲むと自由に書けるようになるので、テスト失敗したときに何で失敗したかわかりやすくてとてもいいなと思います。


@Test
fun `getSelectedContentTitle - Collect Title Success`() = runTest {
    val state = TrainingContentState(selectedIndex = 1, currentPageIndex = 0)
    val title = viewModel.getSelectedContentTitle(state)
    val expected = "ダミーの学習タイトル"

    // モックが正しく呼ばれていることを確認
    runBlocking {
        Mockito.verify(mockRepository).getAllTrainingContents()
    }

    assertEquals(expected, title)
}

やっていることはシンプルで、テスト対象のメソッドのgetSelectedContentTitleにわたす引数を用意し実際に渡して、assertEqualsで期待値のexpectedと比較してOK/NGを判定しています。

また、モックを使っているため、Mockito.verifyでViewModel初期化時に呼ばれるgetAllTrainingContents()が正しく呼ばれていることを確認しています。これが失敗していると、リポジトリの作成に失敗しているため、テストの前提が成立しないためです。

テスト実行

テスト実行はAndroid Studioだとテストクラス名の左横のボタンを押して、以下でRun 'TrainingView...' をクリックするとテスト実行します。簡単ですね。

結果は以下のように出ます。

成功時(オールグリーンになるだけ)

失敗時(わざと失敗させてみた。どこで失敗したかが出る)

まとめ

本記事ではAndroidのユニットテストについて紹介しました。Andorid XRにおいても、今までのAndroid(Kotlin)のユニットテストのノウハウがそのまま活かせるため、Androidエンジニアがとても参入しやすいかなと思います。

また、普通のAndroidスマホ開発と同様、こういったユニットテストはCIで回したいため、GitHub Actionsなどで自動化したいところです。自動化はまた別の記事で紹介できればと思います。