Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
コルーチンのエラーをテストするためのTips / Tips for testing Kotli...
Search
tkmnzm
March 03, 2023
Programming
1.3k
0
Share
コルーチンのエラーをテストするためのTips / Tips for testing Kotlin Coroutine errors
DeNA.apk#4(
https://dena.connpass.com/event/274954/)の発表資料です
。
tkmnzm
March 03, 2023
More Decks by tkmnzm
See All by tkmnzm
AndroidアプリのUIバリエーションをあの手この手で確認する / Check UI variations of Android apps by various means
tkmnzm
1
1.6k
Androidアプリの良いユニットテストを考える / Thinking about good unit tests for Android apps
tkmnzm
5
9.9k
Google I:O 2023 Androidの自動テストアップデートまとめ / Google I:O 2023 Android Testing Update Recap
tkmnzm
0
680
Androidのモダンな技術選択にあわせて自動テストも アップデートしよう / Update your automated tests to match Android's modern technology choices
tkmnzm
3
2.5k
SWET dev-vitalチームによるプロジェクトの健康状態可視化の取り組み / SWET dev-vital team's efforts to visualize the health of the project
tkmnzm
1
1.4k
モバイルアプリテスト入門 / Getting Started with Mobile App Testing
tkmnzm
1
620
25分で作るAndroid Lint / Android Lint made in 25 minutes
tkmnzm
0
1k
2年半ぶりのプロダクト開発であらためて感じた自動テストの大切さ / realized the importance of automatic testing with product development for the first time in two and a half years
tkmnzm
1
860
Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題 / Android Screenshot Test Problems solved by introducing into 3 products
tkmnzm
2
1.3k
Other Decks in Programming
See All in Programming
アクセシビリティ試験の"その後"を仕組み化する
yuuumiravy
0
150
Swift Concurrency Type System
inamiy
0
530
PDI: Como Alavancar Sua Carreira e Seu Negócio
marcelgsantos
0
120
HTML-Aware ERB: The Path to Reactive Rendering @ RubyKaigi 2026, Hakodate, Japan
marcoroth
0
140
Server-Side Kotlin LT大会 vol.18 [Kotlin-lspの最新情報と Neovimのlsp設定例]
yasunori0418
1
150
AIベース静的検査器の偽陽性率を抑える工夫3選
orgachem
PRO
3
320
Making the RBS Parser Faster
soutaro
0
390
おれのAgentic Coding 2026/03
tsukasagr
1
150
forteeの改修から振り返るPHPerKaigi 2026
muno92
PRO
3
290
Liberating Ruby's Parser from Lexer Hacks
ydah
2
1.3k
Lightning-Fast Method Calls with Ruby 4.1 ZJIT / RubyKaigi 2026
k0kubun
3
420
AI時代のPhpStorm最新事情 #phpcon_odawara
yusuke
0
190
Featured
See All Featured
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Faster Mobile Websites
deanohume
310
31k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
122
21k
Navigating the Design Leadership Dip - Product Design Week Design Leaders+ Conference 2024
apolaine
0
270
Un-Boring Meetings
codingconduct
0
270
How to train your dragon (web standard)
notwaldorf
97
6.6k
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
340
Prompt Engineering for Job Search
mfonobong
0
270
Breaking role norms: Why Content Design is so much more than writing copy - Taylor Woolridge
uxyall
0
260
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
1
200
VelocityConf: Rendering Performance Case Studies
addyosmani
333
25k
Side Projects
sachag
455
43k
Transcript
コルーチンのエラーをテストする ためのTips DeNA.apk#4 Nozomi Takuma
自己紹介 • Nozomi Takuma • DeNA SWETグループ ◦ 兼務: Pococha事業部システム部
• Androidとテストが好き
今日話すこと
今日話すこと • suspend関数からthrowされるExceptionをテストする • ViewModelScope内でのExceptionの振る舞い
suspend関数から throwされるExceptionをテストする
テスト対象のコード class NewsRepository( private val networkDataSource: NetworkDataSource ) { suspend
fun getNewsResources(): List<NewsResource> { return networkDataSource.getNewsResources() } } API通信をするsuspend関数 APIからエラーが返ってきたときは そのままthrowされる
Exceptionをthrowする関数のテストの書き方① @Test(expected = HttpException::class) fun getNewsResources() = runTest { //
省略. APIエラーを返すスタブの設定 val newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() }
Exceptionをthrowするsuspend関数のテストの書き方① @Test(expected = HttpException::class) fun getNewsResources() = runTest { //
省略. APIエラーを返すスタブの設定 val newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() } Exceptionがthrowされるsuspend 関数を実行
Exceptionをthrowするsuspend関数のテストの書き方① @Test(expected = HttpException::class) fun getNewsResources() = runTest { //
省略. APIエラーを返すスタブの設定 val newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() } Throwされるクラスを指定 同じ型のThrowableが投げられたら成功 Throwableが投げられなかったり、違う 型だった場合はテスト失敗
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) }
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } Junit4に入っているThrowable用の アサーション ブロックの中でthrowする関数を呼び出す
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } Throwableの型 + インスタンス に対してアサートをすることが できる
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) }
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertThrows(HttpException::class.java) { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } Junit4のassertThrowsのブロックの中で は直接suspend関数を呼び出せない (Junit5のassertThrowsは可)
Exceptionをthrowするsuspend関数のテストの書き方② build.gradle testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertFailWith<HttpException> { newsRepository.getNewsResources() } assertEquals("error", exception.message()) }
Exceptionをthrowするsuspend関数のテストの書き方② @Test fun getNewsResources() = runTest { .. val exception
= assertFailWith<HttpException> { newsRepository.getNewsResources() } assertEquals("error", exception.message()) } インライン関数なのでTestScopeの中で 実行できる
ViewModelScope内でのExceptionの振る舞い
テスト対象のコード class NewsViewModel : ViewModel() { fun bookmarkNews(newsResourceId: String, bookmarked:
Boolean) { viewModelScope.launch { throw IOException() } } }
テスト対象のコード class NewsViewModel : ViewModel() { fun bookmarkNews(newsResourceId: String, bookmarked:
Boolean) { viewModelScope.launch { throw IOException() } } } viewModelScope内で例外が発生 エラーハンドリングをし忘れている
テストコード @Test fun bookmarkNews() { viewModel.bookmarkNews("id", true) }
テストコード @Test fun bookmarkNews() { viewModel.bookmarkNews("id", true) }
テストコード @Test fun bookmarkNews() { viewModel.bookmarkNews("id", true) } 例外が握りつぶされている
ViewModelで問題になるのはなぜ? @Test(expected = HttpException::class) fun getNewsResources() = runTest { val
newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() }
ViewModelで問題になるのはなぜ? @Test(expected = HttpException::class) fun getNewsResources() = runTest { val
newsRepository = NewsRepository(testDataSource) newsRepository.getNewsResources() } ブロックの中はTestScopeで実行される
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) ) }
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) ) }
関連するIssue TestCoroutineDispatcher swallows exceptions #1205 https://github.com/Kotlin/kotlinx.coroutines/issues/1205
ViewModel 2.5.0でのアップデート public ViewModel(@NonNull Closeable... closeables) { mCloseables.addAll(Arrays.asList(closeables)); }
ViewModel 2.5.0でのアップデート public ViewModel(@NonNull Closeable... closeables) { mCloseables.addAll(Arrays.asList(closeables)); } onClearで閉じられる
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を用意
ViewModel 2.5.0でのアップデート class NewsViewModel( val customScope: CloseableCoroutineScope = CloseableCoroutineScope() )
: ViewModel(customScope) { fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) { customScope.launch { throw IOException() } } }
ViewModel 2.5.0でのアップデート class NewsViewModel( val customScope: CloseableCoroutineScope = CloseableCoroutineScope() )
: ViewModel(customScope) { fun bookmarkNews(newsResourceId: String, bookmarked: Boolean) { customScope.launch { throw IOException() } } } ViewModelScopeと同じように使える
テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =
CloseableCoroutineScope(coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) }
テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =
CloseableCoroutineScope(coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) } TestScopeから ClosableCoroutineScopeを作る
テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =
CloseableCoroutineScope(coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) } ViewModelに渡す
テストコードの変更 @Test fun bookmarkNews() = runTest { val scope =
CloseableCoroutineScope(this.coroutineContext + UnconfinedTestDispatcher()) val newsViewModel = NewsViewModel(scope) newsViewModel.bookmarkNews("id", true) }
まとめ
今日話したこと • suspend関数からthrowされるExceptionをテストする ◦ @Test(expected = Throwable) ◦ Kotlin Test
LibraryのassertFailWithも便利 • ViewModelScope内でのExceptionの振る舞い ◦ Exception発生時にテストをコケさせるためには工夫がいる ◦ エラーハンドリング漏れには注意しよう
ご清聴ありがとうございました!