SubwayTooter-Android-App/base/src/androidTest/java/jp/juggler/DispatchersTest.kt

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{ ... }
}