CoroutineでDispatcherを使う場合、直接指定するわけではなくInjectすることが推奨されています。
GoogleのSampleでは以下のコードが記載されていますが、HiltをつかってInjectしている場合、初期値を使えないのでこのままでは動かせません。
// DO inject Dispatchers
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}
// DO NOT hardcode Dispatchers
class NewsRepository {
// DO NOT use Dispatchers.Default directly, inject it instead
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}
なので、以下のようなModuleを使ってあげるとAnnotationを使って簡単に使用したいDispatcherをInjectすることができます。
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class DefaultDispatcher
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class MainDispatcher
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class UnconfinedDispatcher
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IoDispatcher
@Module
class DispatcherModule {
@DefaultDispatcher
@Provides
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@MainDispatcher
@Provides
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@UnconfinedDispatcher
@Provides
fun provideUnconfinedDispatcher(): CoroutineDispatcher = Dispatchers.Unconfined
@IoDispatcher
@Provides
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
}
class NewsRepository @Inject constructor(
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}
さて、このクラスをテストする場合、GoogleのSampleでは以下のコードが記載されています。
class ArticlesRepositoryTest {
@Test
fun testBookmarkArticle() = runTest {
// Pass the testScheduler provided by runTest's coroutine scope to
// the test dispatcher
val testDispatcher = UnconfinedTestDispatcher(testScheduler)
val articlesDataSource = FakeArticlesDataSource()
val repository = ArticlesRepository(
articlesDataSource,
testDispatcher
)
val article = Article()
repository.bookmarkArticle(article)
assertThat(articlesDataSource.isBookmarked(article)).isTrue()
}
}
しかし、この動かし方だと、毎回テストするクラスとDispatcherを作る必要がありとても面倒です。
なので、Test用のDispatcherを最初にInjectして動かせば良さそうですが、エラーが出て動きません。
class ArticlesRepositoryTest {
private val testDispatcher = UnconfinedTestDispatcher(testScheduler)
private val repository = ArticlesRepository(
articlesDataSource,
testDispatcher
)
@Test
fun testBookmarkArticle() = runTest {
val article = Article()
repository.bookmarkArticle(article)
assertThat(articlesDataSource.isBookmarked(article)).isTrue()
}
}
Detected use of different schedulers. If you need to use several test coroutine dispatchers, create one
TestCoroutineScheduler
and pass it to each of them
これはrunTestで別のDispatcherで動いているためエラーとなるようです。
なので、runTest時に動かすDispatcherを指定してあげることで解消されます。
class ArticlesRepositoryTest {
private val testDispatcher = UnconfinedTestDispatcher(testScheduler)
private val repository = ArticlesRepository(
articlesDataSource,
testDispatcher
)
@Test
fun testBookmarkArticle() = runTest(testDispatcher) {
val article = Article()
repository.bookmarkArticle(article)
assertThat(articlesDataSource.isBookmarked(article)).isTrue()
}
}
これでいい感じにテストができるようになりました。良かったですね。
参考