Upgrade to Pro — share decks privately, control downloads, hide ads and more …

コルーチンのエラーをテストするためのTips / Tips for testing Kotlin Coroutine errors

tkmnzm
March 03, 2023

コルーチンのエラーをテストするためのTips / Tips for testing Kotlin Coroutine errors

tkmnzm

March 03, 2023
Tweet

More Decks by tkmnzm

Other Decks in Programming

Transcript

  1. テスト対象のコード class NewsRepository( private val networkDataSource: NetworkDataSource ) { suspend

    fun getNewsResources(): List<NewsResource> { return networkDataSource.getNewsResources() } } API通信をするsuspend関数 APIからエラーが返ってきたときは そのままthrowされる
  2. Exceptionをthrowする関数のテストの書き方① @Test(expected = HttpException::class) fun getNewsResources() = runTest { //

    省略. APIエラーを返すスタブの設定 val newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() }
  3. Exceptionをthrowするsuspend関数のテストの書き方① @Test(expected = HttpException::class) fun getNewsResources() = runTest { //

    省略. APIエラーを返すスタブの設定 val newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() } Exceptionがthrowされるsuspend 関数を実行
  4. Exceptionをthrowするsuspend関数のテストの書き方① @Test(expected = HttpException::class) fun getNewsResources() = runTest { //

    省略. APIエラーを返すスタブの設定 val newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() } Throwされるクラスを指定 同じ型のThrowableが投げられたら成功 Throwableが投げられなかったり、違う 型だった場合はテスト失敗
  5. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) }
  6. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } Junit4に入っているThrowable用の アサーション ブロックの中でthrowする関数を呼び出す
  7. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } Throwableの型 + インスタンス に対してアサートをすることが できる
  8. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) }
  9. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } Junit4のassertThrowsのブロックの中で は直接suspend関数を呼び出せない (Junit5のassertThrowsは可)
  10. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertFailWith<HttpException> { newsRepository.getNewsResources() } assertEquals("error", exception.message()) }
  11. Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception

    = assertFailWith<HttpException> { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } インライン関数なのでTestScopeの中で 実行できる
  12. テスト対象のコード class NewsViewModel : ViewModel() { fun bookmarkNews(newsResourceId: String, bookmarked:

    Boolean) { viewModelScope.launch { throw IOException() } } } viewModelScope内で例外が発生 エラーハンドリングをし忘れている
  13. ViewModelで問題になるのはなぜ? @Test(expected = HttpException::class) fun getNewsResources() = runTest { val

    newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() }
  14. ViewModelで問題になるのはなぜ? @Test(expected = HttpException::class) fun getNewsResources() = runTest { val

    newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() } ブロックの中はTestScopeで実行される
  15. ViewModelで問題になるのはなぜ? public val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope?

    = this.getTag(JOB_KEY) if (scope != null) { return scope } return setTagIfAbsent( JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) ) }
  16. ViewModelで問題になるのはなぜ? public val ViewModel.viewModelScope: CoroutineScope get() { val scope: CoroutineScope?

    = this.getTag(JOB_KEY) if (scope != null) { return scope } return setTagIfAbsent( JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) ) }
  17. ViewModel 2.5.0でのアップデート class CloseableCoroutineScope( context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate

    ) : Closeable, CoroutineScope { override val coroutineContext: CoroutineContext = context override fun close() { coroutineContext.cancel() } } Closableを実装した CoroutineScopeを用意
  18. ViewModel 2.5.0でのアップデート class NewsViewModel( val customScope: CloseableCoroutineScope = CloseableCoroutineScope() )

    : ViewModel(customScope) { fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) { customScope.launch { throw IOException() } } }
  19. ViewModel 2.5.0でのアップデート class NewsViewModel( val customScope: CloseableCoroutineScope = CloseableCoroutineScope() )

    : ViewModel(customScope) { fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) { customScope.launch { throw IOException() } } } ViewModelScopeと同じように使える
  20. テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =

    CloseableCoroutineScope(coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) }
  21. テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =

    CloseableCoroutineScope(coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) } TestScopeから ClosableCoroutineScopeを作る
  22. テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =

    CloseableCoroutineScope(coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) } ViewModelに渡す
  23. テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =

    CloseableCoroutineScope(this.coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) }
  24. 今日話したこと • suspend関数からthrowされるExceptionをテストする ◦ @Test(expected = Throwable) ◦ Kotlin Test

    LibraryのassertFailWithも便利 • ViewModelScope内でのExceptionの振る舞い ◦ Exception発生時にテストをコケさせるためには工夫がいる ◦ エラーハンドリング漏れには注意しよう