
みなさまこんにちは〜!
メモリアインクのすだです。
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内でのコルーチンテスト
ここでは、以下の処理があると想定してテストコードを書いていきます、
UserRepository
に suspend 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内でのコルーチンテスト
ここでは、以下の処理があると想定してテストコードを書いていきます、
UserRepository
に fun 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エンジニアを目指すなら【ユニゾンキャリア】>
自分の市場価値をさらに向上させてみませんか?
それではまた次回の記事でお会いしましょう!
コメント