167 lines
6.0 KiB
Kotlin
167 lines
6.0 KiB
Kotlin
package jp.juggler
|
|
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
import jp.juggler.base.TestDispatcherRule
|
|
import jp.juggler.util.coroutine.AppDispatchers
|
|
import kotlinx.coroutines.*
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
|
import kotlinx.coroutines.test.advanceUntilIdle
|
|
import kotlinx.coroutines.test.runTest
|
|
import org.junit.Rule
|
|
import org.junit.Test
|
|
import org.junit.Assert.*
|
|
import org.junit.runner.RunWith
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
|
|
/**
|
|
* kotlinx.coroutines.test の使い方の説明
|
|
* https://developer.android.com/kotlin/coroutines/test?hl=ja#testdispatchers
|
|
*/
|
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
@RunWith(AndroidJUnit4::class)
|
|
class DispatchersTest {
|
|
|
|
// 単純なリポジトリ
|
|
private class UserRepository {
|
|
val names = ArrayList<String>()
|
|
fun register(name: String) = names.add(name)
|
|
fun getAllUsers(): List<String> = names
|
|
}
|
|
|
|
// Dispatcherを受け取るリポジトリ
|
|
private class Repository(
|
|
private val ioDispatcher: CoroutineDispatcher = AppDispatchers.IO,
|
|
) {
|
|
private val ioScope = CoroutineScope(ioDispatcher)
|
|
val initialized = AtomicBoolean(false)
|
|
|
|
// A function that starts a new coroutine on the IO dispatcher
|
|
fun initializeAsync() = ioScope.async {
|
|
delay(100L)
|
|
initialized.set(true)
|
|
}
|
|
|
|
// A suspending function that switches to the IO dispatcher
|
|
suspend fun fetchData(): String = withContext(ioDispatcher) {
|
|
require(initialized.get()) { "Repository should be initialized first" }
|
|
delay(500L)
|
|
"Hello world"
|
|
}
|
|
}
|
|
|
|
// プロパティの定義順序に注意
|
|
@get:Rule
|
|
val dispatcheRule = TestDispatcherRule()
|
|
|
|
// リポジトリのスケジューラを共有する
|
|
private val repository = Repository(dispatcheRule.testDispatcher)
|
|
|
|
//====================================================
|
|
// テストでの suspend 関数の呼び出し
|
|
// runTestを使う
|
|
|
|
private suspend fun fetchData(): String {
|
|
delay(1000L)
|
|
return "Hello world"
|
|
}
|
|
|
|
@Test
|
|
fun useRunTest() = runTest {
|
|
assertEquals("Hello world", fetchData())
|
|
}
|
|
|
|
//====================================================
|
|
// launch内部の処理を待つテストコード
|
|
|
|
@Test
|
|
fun useAdvanceUntilIdle() = runTest {
|
|
val userRepo = UserRepository()
|
|
launch { userRepo.register("Alice") }
|
|
launch { userRepo.register("Bob") }
|
|
advanceUntilIdle() // Yields to perform the registrations
|
|
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
|
|
}
|
|
|
|
//==============================================
|
|
// UnconfinedTestDispatcher を使うとlaunch内部が先に実行開始する
|
|
// ただしlaunch内部で非同期待機が入ると外側の実行が再開される
|
|
|
|
@Test
|
|
fun useUnconfinedTestDispatcher() = runTest(UnconfinedTestDispatcher()) {
|
|
val userRepo = UserRepository()
|
|
launch { userRepo.register("Alice") }
|
|
launch { userRepo.register("Bob") }
|
|
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
|
|
}
|
|
|
|
// =============================================
|
|
// viewModelScopeなどが使うディスパッチャーを差し替える
|
|
|
|
class HomeViewModel : ViewModel() {
|
|
private val _message = MutableStateFlow("")
|
|
val message: StateFlow<String> get() = _message
|
|
|
|
fun loadMessage() {
|
|
viewModelScope.launch {
|
|
_message.value = "Greetings!"
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun useDispatchersSetMain() = runTest {
|
|
// MainDispatcherRule を指定しているので、viewModelが使う Dispatcher が変わる
|
|
val viewModel = HomeViewModel()
|
|
viewModel.loadMessage()
|
|
assertEquals("Greetings!", viewModel.message.value)
|
|
}
|
|
|
|
// =============================================================
|
|
// リポジトリクラスにDispatcherを渡せるようにする
|
|
|
|
@Test
|
|
fun useRepoWithTestDispatcher() = runTest {
|
|
val repository = Repository(
|
|
ioDispatcher = StandardTestDispatcher(testScheduler)
|
|
)
|
|
repository.initializeAsync().await()
|
|
assertEquals(true, repository.initialized.get())
|
|
assertEquals("Hello world", repository.fetchData())
|
|
}
|
|
|
|
//=======================================================
|
|
// プロパティ間でスケジューラを共有する
|
|
|
|
@Test
|
|
fun someRepositoryTest() = runTest {
|
|
// Takes scheduler from Main
|
|
|
|
// Any TestDispatcher created here also takes the scheduler from Main
|
|
// val newTestDispatcher = StandardTestDispatcher()
|
|
|
|
// これもStandardTestDispatcher を作成する
|
|
// 注意: 独自の TestScope を作成する場合は、テスト内のそのスコープで runTest を呼び出す必要があります。
|
|
// テストには TestScope インスタンスを 1 つだけ含めることができます。
|
|
// val testScope = TestScope()
|
|
|
|
repository.initializeAsync().await()
|
|
assertEquals(true, repository.initialized.get())
|
|
assertEquals("Hello world", repository.fetchData())
|
|
}
|
|
|
|
//=======================================================
|
|
// DI
|
|
// クラス内に以下のようなプロパティを定義しておくこともできる。
|
|
// DIする際は参考になるかもしれない。
|
|
// val testScheduler = TestCoroutineScheduler()
|
|
// val testDispatcher = StandardTestDispatcher(testScheduler)
|
|
// val testScope = TestScope(testDispatcher)
|
|
//
|
|
// fun xxx() = testScope.runTest{ ... }
|
|
}
|