【正社員】還元率83%【フリーランス】マージン一律5万円で案件をご紹介させていただきます。 詳細はこちら

【Android】<Kotlin>コルーチンとFlowのモックテスト完全ガイド|JUnit5×MockKで非同期処理を検証する

【Android】<Kotlin>コルーチンとFlowのモックテスト完全ガイド|JUnit5×MockKで非同期処理を検証する
すだ

みなさまこんにちは〜!
メモリアインクのすだです。

KotlinでAndroidアプリを開発していると、
「非同期処理のテストってどう書けばいいの?」と悩んだことはありませんか?
特に、suspend 関数や Flow を使ったViewModelのテストは、
一見難しそうに見えます。
この記事では、JUnit5 + MockK + コルーチンテスト環境を使って、
非同期処理のテストをわかりやすく解説します。

この記事を読んでわかること…
・JUnit5 + MockKを使った非同期処理(コルーチン)のテスト方法

JUnit5やMockKに関する基本的な知識に関しては、以下の記事でご紹介しています。
ぜひ併せてご覧ください。

目次

環境

  • Kotlin (ver 1.9.0)
  • Android Studio (Giraffe | 2022.3.1 Patch 3)

テスト環境と依存関係の準備

JUnit5とMockKを使って非同期処理(コルーチン)のテストを行うにあたって、いくつか準備が必要です。

build.gradle.kts に以下の依存関係を追加してください:

dependencies {
    // Kotlin Coroutine テスト
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

    // MockK(Kotlin専用のモックライブラリ)
    testImplementation("io.mockk:mockk:1.13.8")

    // JUnit5(プラットフォーム + API)
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}

MockK:Kotlin向けのモックライブラリで、非同期関数もモック化できます。

kotlinx-coroutines-test:コルーチンのテストを簡単にする公式ライブラリ。

JUnit5:最新のユニットテストフレームワーク。

コルーチンのテスト(suspend関数)

コルーチンとは、軽量な非同期処理のためのKotlinの仕組みです。
suspend fun はコルーチンスコープでのみ実行できるため、テストでは runTest {} を使ってブロック内で呼び出します。

実践:ViewModel内でのコルーチンテスト

ここでは、以下の処理があると想定してテストコードを書いていきます、

UserRepositorysuspend fun fetchUserName(): String があり、
ProfileViewModel がそれを使ってユーザー名を保持する。

ViewModel▼

class ProfileViewModel(private val repository: UserRepository) : ViewModel() {

    private val _userName = MutableStateFlow<String>("")
    val userName: StateFlow<String> = _userName.asStateFlow()

    fun loadUserName() {
        viewModelScope.launch {
            try {
                val result = withContext(Dispatchers.IO) {
                    repository.fetchUserName()
                }
                _userName.value = result
            } catch (e: Exception) {
                _userName.value = "エラーが発生しました"
            }
        }
    }
}

テストコード(JUnit5 + MockK)▼

@OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewModelTest {

    @MockK
    lateinit var repository: UserRepository

    private lateinit var viewModel: ProfileViewModel

    private val testDispatcher = StandardTestDispatcher()

    @BeforeEach
    fun setUp() {
        MockKAnnotations.init(this)
        Dispatchers.setMain(testDispatcher)  // Main Dispatcher を差し替え
        viewModel = ProfileViewModel(repository)
    }

    @AfterEach
    fun tearDown() {
        Dispatchers.resetMain()  // Dispatcher の状態を戻す
    }

    @Test
    fun `ユーザー名が正常に取得される`() = runTest {
        // Arrange
        coEvery { repository.fetchUserName() } returns "山田太郎"

        // Act
        viewModel.loadUserName()
        advanceUntilIdle() // 全コルーチンの完了を待つ

        // Assert
        assertEquals("山田太郎", viewModel.userName.value)
        coVerify { repository.fetchUserName() }
    }
}

@OptIn(…)

@OptIn(...) は、まだ「実験的(Experimental)」なAPIを使う場合の宣言です。
kotlinx.coroutines.test.StandardTestDispatcher などの一部のテストAPIは実験的なので、明示的にこのアノテーションを付ける必要があります。

StandardTestDispatcher()

通常の Dispatchers.Main を置き換えるためのテスト用ディスパッチャーです。
これによりテストの中で非同期処理の進行を自分で制御できるようになります。

Dispatchers.setMain(dispatcher)

viewModelScope などが内部で使う Dispatchers.Main を、テスト用の dispatcher にすり替えます。
これにより、非同期処理をテスト環境で制御できるようになります。

Dispatchers.resetMain()

テスト後にコルーチンのディスパッチャーを元の状態に戻す処理です。
特に Dispatchers.setMain(...) を使ったテスト環境では、必ず必要な後始末になります。
(これをしないままだと、他のテストやアプリ全体に影響を与えてしまうため)

runTest { … }

runTest は Kotlin Coroutines の公式テストライブラリ(kotlinx-coroutines-test)に含まれている、
テスト用の仮想的なコルーチンスコープを提供する関数です。

coEvery

suspend 関数をモック化するためのMockK専用関数です。
{}内でモック化したいsuspend関数を呼び出して、returnsで呼び出し時に返す値を指定します。

advanceUntilIdle()

「今動いているすべてのコルーチンが完了するまで、仮想時間を進めて待つ」関数です。
launch の中は テスト実行時にはまだ終わっていないことが多いため、テストで即座に assertEquals(...) を書いても、非同期処理がまだ終わってなくてテスト失敗する可能性があります。
そのため、advanceUntilIdle()を使って仮想的に「時間を進めて」すべてのコルーチンが完了した状態にしてくれるという実装を行う必要があります。

Flowのテスト

Flow は「非同期で連続的なデータの流れ」を表現する仕組みです。
テストでは flowOf(...) などで簡単にモックできます。

実践:ViewModel内でのコルーチンテスト

ここでは、以下の処理があると想定してテストコードを書いていきます、

UserRepositoryfun fetchUserNameFlow(): Flow<String> があり、
ProfileViewModel がそれを StateFlow に変換して公開している。

ViewModel▼

class ProfileViewModel(private val repository: UserRepository) {
    val userNameFlow: StateFlow<String> = repository.fetchUserNameFlow()
        .stateIn(
            scope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
            started = SharingStarted.Eagerly,
            initialValue = ""
        )
}

テストコード(JUnit5 + MockK)▼

@OptIn(ExperimentalCoroutinesApi::class)
class ProfileViewModelFlowTest {

    @MockK
    lateinit var repository: UserRepository

    lateinit var viewModel: ProfileViewModel

    private val dispatcher = StandardTestDispatcher()

    @BeforeEach
    fun setUp() {
        MockKAnnotations.init(this)
        Dispatchers.setMain(dispatcher)
    }

    @AfterEach
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `Flowでユーザー名が流れる`() = runTest {
        every { repository.fetchUserNameFlow() } returns flowOf("サトウ")

        viewModel = ProfileViewModel(repository)

        val result = viewModel.userNameFlow.first()
        assertEquals("サトウ", result)
    }
}

flowOf()

Flow の要素を指定して、すぐに作れる簡易な Flow ビルダー関数です。
たとえば、flowOf("A", "B", "C")と書けば、”A” → “B” → “C” と順に値を流す Flow を作れます。
つまり、決まった値を流すだけのシンプルな Flow を1行で作れるのが特徴です。

今回のテストコードでは、「repository.fetchUserNameFlow() が呼ばれたとき、’サトウ’ という値だけを流す Flow を返すようにモックする」という意味になります。

まとめ

おつかれさまでした。いかがでしたでしょうか!

JUnit5 + MockK + Coroutines Test を使えば、非同期処理もシンプルにテストできます。
ぜひ活用してみてください!

すだ

技術者としてのキャリアパスを次のレベルへと進めたい皆様、
未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】
を通じて、
自分の市場価値をさらに向上させてみませんか?

それではまた次回の記事でお会いしましょう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

弊社テックブログをご愛読いただきありがとうございます。
当テックブログを運用している株式会社メモリアインクは、
【正社員】還元率83%
【フリーランス】マージン一律5万円で案件のご紹介
と、エンジニアの皆様に分かりやすい形で稼げる仕組みを構築し提供させていただいております。

コメント

コメントする

目次