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
0
1.1k
コルーチンのエラーをテストするためのTips / Tips for testing Kotlin Coroutine errors
DeNA.apk#4(
https://dena.connpass.com/event/274954/)の発表資料です
。
tkmnzm
March 03, 2023
Tweet
Share
More Decks by tkmnzm
See All by tkmnzm
AndroidアプリのUIバリエーションをあの手この手で確認する / Check UI variations of Android apps by various means
tkmnzm
1
1.3k
Androidアプリの良いユニットテストを考える / Thinking about good unit tests for Android apps
tkmnzm
5
8.8k
Google I:O 2023 Androidの自動テストアップデートまとめ / Google I:O 2023 Android Testing Update Recap
tkmnzm
0
630
Androidのモダンな技術選択にあわせて自動テストも アップデートしよう / Update your automated tests to match Android's modern technology choices
tkmnzm
3
2.3k
SWET dev-vitalチームによるプロジェクトの健康状態可視化の取り組み / SWET dev-vital team's efforts to visualize the health of the project
tkmnzm
1
1.3k
モバイルアプリテスト入門 / Getting Started with Mobile App Testing
tkmnzm
1
570
25分で作るAndroid Lint / Android Lint made in 25 minutes
tkmnzm
0
940
2年半ぶりのプロダクト開発であらためて感じた自動テストの大切さ / realized the importance of automatic testing with product development for the first time in two and a half years
tkmnzm
1
810
Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題 / Android Screenshot Test Problems solved by introducing into 3 products
tkmnzm
2
1.2k
Other Decks in Programming
See All in Programming
Amazon Q CLI開発で学んだAIコーディングツールの使い方
licux
3
180
変化を楽しむエンジニアリング ~ いままでとこれから ~
murajun1978
0
680
書き捨てではなく継続開発可能なコードをAIコーディングエージェントで書くために意識していること
shuyakinjo
0
220
Gemini CLIの"強み"を知る! Gemini CLIとClaude Codeを比較してみた!
kotahisafuru
3
940
コーディングは技術者(エンジニア)の嗜みでして / Learning the System Development Mindset from Rock Lady
mackey0225
2
240
PHPカンファレンス関西2025 基調講演
sugimotokei
6
1.1k
物語を動かす行動"量" #エンジニアニメ
konifar
12
3.4k
DataformでPythonする / dataform-de-python
snhryt
0
160
ZeroETLで始めるDynamoDBとS3の連携
afooooil
0
150
MySQL9でベクトルカラム登場!PHP×AWSでのAI/類似検索はこう変わる
suguruooki
1
290
What's new in Adaptive Android development
fornewid
0
140
オホーツクでコミュニティを立ち上げた理由―地方出身プログラマの挑戦 / TechRAMEN 2025 Conference
lemonade_37
1
450
Featured
See All Featured
Bash Introduction
62gerente
614
210k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
35
2.5k
Raft: Consensus for Rubyists
vanstee
140
7.1k
Writing Fast Ruby
sferik
628
62k
Code Reviewing Like a Champion
maltzj
524
40k
Mobile First: as difficult as doing things right
swwweet
223
9.9k
Stop Working from a Prison Cell
hatefulcrawdad
271
21k
Making Projects Easy
brettharned
117
6.3k
Scaling GitHub
holman
461
140k
Measuring & Analyzing Core Web Vitals
bluesmoon
8
550
What’s in a name? Adding method to the madness
productmarketing
PRO
23
3.6k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
8
880
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発生時にテストをコケさせるためには工夫がいる ◦ エラーハンドリング漏れには注意しよう
ご清聴ありがとうございました!