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
610
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
850
Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題 / Android Screenshot Test Problems solved by introducing into 3 products
tkmnzm
2
1.3k
Other Decks in Programming
See All in Programming
Offline should be the norm: building local-first apps with CRDTs & Kotlin Multiplatform
renaudmathieu
0
180
Coding at the Speed of Thought: The New Era of Symfony Docker
dunglas
0
4.8k
Xdebug と IDE による デバッグ実行の仕組みを見る / Exploring-How-Debugging-Works-with-Xdebug-and-an-IDE
shin1x1
0
360
車輪の再発明をしよう!PHP で実装して学ぶ、Web サーバーの仕組みと HTTP の正体
h1r0
3
520
Rethinking API Platform Filters
vinceamstoutz
0
11k
今年もTECHSCOREブログを書き続けます!
hiraoku101
0
240
10 Tips of AWS ~Gen AI on AWS~
licux
5
250
安いハードウェアでVulkan
fadis
1
940
GNU Makeの使い方 / How to use GNU Make
kaityo256
PRO
16
5.6k
仕様漏れ実装漏れをなくすトレーサビリティAI基盤のご紹介
orgachem
PRO
9
5.4k
AI時代の脳疲弊と向き合う ~言語学としてのPHP~
sakuraikotone
1
1.8k
AIエージェントで業務改善してみた
taku271
0
500
Featured
See All Featured
Reflections from 52 weeks, 52 projects
jeffersonlam
356
21k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
330
Measuring Dark Social's Impact On Conversion and Attribution
stephenakadiri
1
180
How People are Using Generative and Agentic AI to Supercharge Their Products, Projects, Services and Value Streams Today
helenjbeal
1
150
Heart Work Chapter 1 - Part 1
lfama
PRO
5
35k
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.2k
Bridging the Design Gap: How Collaborative Modelling removes blockers to flow between stakeholders and teams @FastFlow conf
baasie
0
510
Why Our Code Smells
bkeepers
PRO
340
58k
Building an army of robots
kneath
306
46k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
490
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
122
21k
Pawsitive SEO: Lessons from My Dog (and Many Mistakes) on Thriving as a Consultant in the Age of AI
davidcarrasco
0
110
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発生時にテストをコケさせるためには工夫がいる ◦ エラーハンドリング漏れには注意しよう
ご清聴ありがとうございました!