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
890
コルーチンのエラーをテストするための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
720
Androidアプリの良いユニットテストを考える / Thinking about good unit tests for Android apps
tkmnzm
5
7.5k
Google I:O 2023 Androidの自動テストアップデートまとめ / Google I:O 2023 Android Testing Update Recap
tkmnzm
0
570
Androidのモダンな技術選択にあわせて自動テストも アップデートしよう / Update your automated tests to match Android's modern technology choices
tkmnzm
3
2.1k
SWET dev-vitalチームによるプロジェクトの健康状態可視化の取り組み / SWET dev-vital team's efforts to visualize the health of the project
tkmnzm
1
1.2k
モバイルアプリテスト入門 / Getting Started with Mobile App Testing
tkmnzm
1
470
25分で作るAndroid Lint / Android Lint made in 25 minutes
tkmnzm
0
840
2年半ぶりのプロダクト開発であらためて感じた自動テストの大切さ / realized the importance of automatic testing with product development for the first time in two and a half years
tkmnzm
1
750
Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題 / Android Screenshot Test Problems solved by introducing into 3 products
tkmnzm
2
1.2k
Other Decks in Programming
See All in Programming
Pinia Colada が実現するスマートな非同期処理
naokihaba
2
140
僕がつくった48個のWebサービス達
yusukebe
18
17k
約9000個の自動テストの 時間を50分->10分に短縮 Flakyテストを1%以下に抑えた話
hatsu38
22
10k
Kaigi on Rails 2024 - Rails APIモードのためのシンプルで効果的なCSRF対策 / kaigionrails-2024-csrf
corocn
5
3.3k
Snowflake x dbtで作るセキュアでアジャイルなデータ基盤
tsoshiro
2
400
推し活の ハイトラフィックに立ち向かう Railsとアーキテクチャ - Kaigi on Rails 2024
falcon8823
6
2k
Vue SFCのtemplateでTypeScriptの型を活用しよう
tsukkee
3
1.5k
弊社の「意識チョット低いアーキテクチャ」10選
texmeijin
5
22k
破壊せよ!データ破壊駆動で考えるドメインモデリング / data-destroy-driven
minodriven
15
3.9k
AWS IaCの注目アップデート 2024年10月版
konokenj
3
2.9k
Importmapを使ったJavaScriptの 読み込みとブラウザアドオンの影響
swamp09
4
1.2k
Progressive Web Apps für Desktop und Mobile mit Angular (Hands-on)
christianliebel
PRO
0
110
Featured
See All Featured
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
27
790
Unsuck your backbone
ammeep
668
57k
A designer walks into a library…
pauljervisheath
202
24k
Fight the Zombie Pattern Library - RWD Summit 2016
marcelosomers
231
17k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
31
2.6k
A Philosophy of Restraint
colly
203
16k
Music & Morning Musume
bryan
46
6.1k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
7
150
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
504
140k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
225
22k
The Cost Of JavaScript in 2023
addyosmani
45
6.6k
Stop Working from a Prison Cell
hatefulcrawdad
267
20k
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発生時にテストをコケさせるためには工夫がいる ◦ エラーハンドリング漏れには注意しよう
ご清聴ありがとうございました!