このセクションでは、次のテストの書き方を紹介しながらKotlin Coroutine及びKotlin Flowのテストの書き方を学ぶ。
※Kotlin Coroutine/Kotlin Flowのバージョンは1.6+
- suspend関数のテストを書く
- APIからエラーが返ってきた時のテストを書く
- Flowが公開されている実装のテストを書く
- Flowを継続的にcollectするテストを書く
- delayを入れたテストを書く
- Dispatcherが指定されたテストを書く
dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
//optional: ハンズオン中のコードの中で登場するアサーションライブラリ
testImplementation "com.google.truth:truth:$truth_version"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}
- テスト対象クラス: DefaultVideoNewsRepository
- テストコード:DefaultVideoNewsRepositoryTest
DefaultVideoNewsRepositoryは2つのメソッドを持っている。
- getVideoNewsResources
- APIから動画ニュースのリストを取得する
- リストが取得されたことを複数の画面に通知するためのFlowを公開する
- downloadVideo
- URLを引数に動画ファイルをダウンロードする
- 同じ動画に対してダウンロードが複数回同時に行われないように、ダウンロードが実行中のURLかを見て、すでに実行中であれば処理をスキップする
DefaultVideoNewsRepositoryから使っているデータソースはNiaNetworkDataSourceで、名前のとおりAPI通信をするデータソース。
そのため、テスト用データソースを用意する方針でテストを実装する。
API通信をするデータソースに対してテスト用のデータソースを用意することは、次のメリットがある。
- API通信をせずにテスト用のデータを返せるようになるため、テストの実行速度とテストの安定性が向上する
- 任意のレスポンスを返せるようにすることで、レスポンスのバリエーションのテストが容易になる
- エラー発生時のテストが書きやすくなる
今回準備したテスト用データソースのコードを抜粋する。
class TestNetworkDataSource(
// テストコードから任意の関数をセットする
// テストコードの中で何度も更新できるようにvarになっている
var getVideoNewsResourcesFunc: (suspend () -> List<NetworkVideoNewsResource>)? = null,
) : NiaNetworkDataSource {
override suspend fun getVideoNewsResources(): List<NetworkVideoNewsResource> =
requireNotNull(getVideoNewsResourcesFunc) { "not set getVideoNewsResources()" }
.invoke()
}
テスト用データソースはテストしたい内容が実現できればどのような実装でも問題ないが、テスト用データソースを作成しやすいようにデータソースのI/Fが定義されていることが重要。
demoExerciseDebug
ビルドバリアントでcore/data/src/testExercise/java/com/google/samples/apps/nowinandroid/core/data/repository/DefaultVideoNewsRepositoryTest.kt
を開いて作業するdemoAnswerDebug
ビルドバリアントに切り替えると解答例を確認できる
runTest
でテストメソッドを囲むことで、テストメソッド内でCoroutineを扱えるようになる。
class VideoNewsRepository() {
// テストしたいsuspend関数
suspend fun getVideoNewsResources(): List<VideoNewsResource> {
return listOf()
}
}
@Test
fun getVideoNewsResources() = runTest { // テストメソッドをrunTestで囲む
val videoNewsRepository = DefaultVideoNewsRepository()
// suspend関数を実行し、戻り値が返る
val actual = videoNewsRepository.getVideoNewsResources()
assertThat(actual).isEqualTo(expected)
}
runTestの特徴は次のとおり。
- テスト用に新しいCoroutineを開始するCoroutineビルダー
- テストコードをラップすることで、Coroutineの中でテストコードを実行する
- 振る舞いはrunBlockingと似ているが、delayを入れても実時間ではその分の時間を待つ必要はない
- Coroutineの実行が他のDispatcherに移動する場合も動作する
- runTest内で直接実行されたsuspend関数は、Dispatcherが切り替わっていた場合でも、戻り値が返ってくるまで待つ
さきほどのコードにdelayを追加してrunTestを実行すると、1秒待つことなくテストが成功する。
runTestのスコープでは時間を仮想時間で扱うことができるため、テストの実行時間を延ばさずに時間の経過をシミュレートできる。
class VideoNewsRepository {
suspend fun getVideoNewsResources(): List<VideoNewsResource> {
delay(1000) // delay
return listOf()
}
}
@Test
fun getVideoNewsResources() = runTest {
val videoNewsRepository = DefaultVideoNewsRepository()
val actual = videoNewsRepository.getVideoNewsResources()
assertThat(actual).isEqualTo(expected)
}
通信処理のライブラリとしてRetrofitを採用しているプロダクトは多い。RetrofitではAPIからエラーが返ってきたときにHttpExceptionをthrowする。
HttpExceptionをcatchしてアプリ用のExceptionに変換して再度throwするコードのテストは次のように書くことができる。
@Test(expected = ApiException::class) // ThrowされるException
fun `getVideoNewsResources_HttpException`() = runTest {
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
videoNewsRepository.getVideoNewsResources() // Exceptionがthrowされるsuspend関数
}
ただし、アノテーションを使った書き方だと、メッセージの中身といったインスタンスに対してのテストはできない。
JunitにはassertThrows
というアサーションもあり、これを使うとThrowableのインスタンスに対してアサーションを追加できる。
ただし、Junit4のassertThrows
をsuspend関数に対して使うと、Suspension functions can be called only within coroutine body
のエラーになる。
そのため、kotlin.test.assertFail
やkotlin.test.assertFailWith
を使うと、suspend関数のテストも楽に書くことができる。
@Test
fun `getVideoNewsResources_HttpException`() = runTest {
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
val throwable = assertFailWith<ApiException> {
videoNewsRepository.getVideoNewsResources() // suspend関数の呼び出し
}
assertThat(throwable.message).isEqualTo("error occurred") // throwableのインスタンスに対してアサーション
}
// TODO
部分を埋めてテストを完成させよう。
- テストメソッド:
getVideoNewsResources_APIからエラーが返ってきた時のテストを書く
- テスト概要:
getVideoNewsResources
からApiExceptionがthrowされることと、ApiExceptionのメッセージを検証する
次のコードは、API通信の結果、videoNewsStream(型: Flow<List<VideoNewsResource>>)
に更新をemitする。
private val _videoNewsStream = MutableSharedFlow<List<VideoNewsResource>>(replay = 1)
val videoNewsStream :SharedFlow<List<VideoNewsResource>> = _videoNewsStream.asSharedFlow()
override suspend fun getVideoNewsResources() {
val videoNewsResource = networkDataSource
.getVideoNewsResources()
.map(NetworkVideoNewsResource::asModel)
_videoNewsStream.emit(videoNewsResource)
}
テストコードでは、videoNewsStream
にemitされた値に対してアサーションを行う。
Flowのfirst()
を呼び出すことで、最初にemitされるアイテムを取得できる。first()
は、1つめのアイテムがemitされるまで待機し、更新を受け取ったらキャンセルする。
@Test
fun getVideoNewsResources() = runTest {
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
videoNewsRepository.getVideoNewsResources()
Truth.assertThat(videoNewsRepository.videoNewsStream.first())
.isEqualTo(listOf(testVideoNewsResource))
}
replayが0のMutableSharedFlowややcapacityが0のChannelは、購読時に値が流れてこない。
private val _videoNewsStream = MutableSharedFlow<List<VideoNewsResource>>()
val videoNewsStream :SharedFlow<List<VideoNewsResource>> = _videoNewsStream.asSharedFlow()
or
private val _videoNewsChannel = Channel<List<VideoNewsResource>>()
val videoNewsStream = _videoNewsChannel.receiveAsFlow()
このとき、Flowのfirst()
を使って検証をしても、値が流れずにタイムアウトしてしまう。値が残らないワンショットのイベントとしてFlowを使う場合は、「Flowを継続的にcollectするテストを書く」のセクションを参考にテストを実装する。
// TODO
部分を埋めてテストを完成させよう。
- テストメソッド:
getVideoNewsResourcesStream_Flowが公開されている実装のテストを書く
- テスト概要:
getVideoNewsResources
を呼び出すとvideoNewsStream
に更新が通知されることを検証する
first()
といったFlowのAPIを使って検証する以外にも、テストコード側でFlowを継続的にcollectして、collectした結果を見るテストを書くこともできる。
@Test
fun getVideoNewsResources() = runTest {
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
// collectした結果を入れるリスト
val actual = mutableListOf<List<VideoNewsResource>>()
// videoNewsStreamをcollectするために、テストコード内でコルーチンを起動する
// UnconfinedTestDispatcherを指定することで、即座にコルーチンが起動される
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
// テストコードでvideoNewsStreamをcollectし、結果をリストに追加していく
videoNewsRepository.videoNewsStream.toCollection(actual)
}
// 2回テストしたいメソッドを実行する. 2回videoNewsStreamにemitされる
videoNewsRepository.refreshVideoNews()
videoNewsRepository.refreshVideoNews()
// 2回分のアイテムをassertする
Truth.assertThat(actual)
.isEqualTo(listOf(
listOf(testVideoNewsResource),
listOf(testVideoNewsResource)
))
// collectしているjobをキャンセルする
job.cancel()
}
上記コードについて、いくつかのポイントを説明する。
collectはsuspend関数なので、完了するまで次の処理が進まない。そのため、新しいCoroutineを起動してその中でcollectする。runTestはレシーバーがTestScope(テスト用のCoroutine Scope)になっており、runTestブロックの中では新しいCoroutineを起動できる。
また、DispatcherにはUnconfinedTestDispatcherを指定し、即座にCoroutineが起動されるようにする。
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.videoNewsStream.toCollection(actual)
}
// テストの実行
job.cancel()
collectしているjobはテスト終了時にキャンセルする必要がある。runTestは自身のScope内に終了していないCoroutineがあると、終了するまで待機するため、キャンセルしないとタイムアウトでテストが失敗する。
backgroundScope
を使用することで、jobをキャンセルする記述を省略できる。backgroundScopeは
、テスト終了時に自動でキャンセルされるテスト用のCoroutineScope。
ループとdelay
を組み合わせた定期実行処理のsuspend関数をテストするといった場合も、backgroundScope
を使うと簡単にテストが実装できる。
// backgroundScopeでlaunch
// 自動でキャンセルされるため、明示的なキャンセルの記述は不要
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.videoNewsStream.toCollection(actual)
}
Coroutine起動時に指定したUnconfinedTestDispatcherはテスト用のDispatcher。kotlinx-coroutines-testパッケージでは、2つのテスト用Dispatcherが提供されている。
- StandardTestDispatcher
- Coroutineをキューに追加し、テストスレッドが空いているときに実行する
- runTestはデフォルトでStandardTestDispatcherを使う
- UnconfinedTestDispatcher
- Coroutineをすぐに実行する
UnconfinedTestDispatcherを指定してCoroutineを起動した場合、処理の順番としては次のようになる。
@Test
fun getVideoNewsResources() = runTest {
// ① テストコードセットアップ
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
// ② videoNewsStreamのcollectのセットアップ
videoNewsRepository.videoNewsStream.toCollection(actual)
}
// ③ テスト対象メソッドの実行 + アサーション
videoNewsRepository.refreshVideoNews()
// 以下省略
}
一方、launch時に何も指定しない(= StandardTestDispatcherを指定した)場合は、collectより前にテスト対象メソッドの実行とアサーションが実行される。 そのため、collect結果を格納するリストが空になっており、テストに失敗する。
@Test
fun getVideoNewsResources() = runTest {
// ① テストコードセットアップ
val job = launch { // StandardTestDispatcher
// ③ videoNewsStreamのcollectのセットアップ
videoNewsRepository.videoNewsStream.toCollection(actual)
}
// ② テスト対象メソッドの実行 + アサーション
videoNewsRepository.refreshVideoNews()
// 以下省略
}
これら2つのTestDispatcherは、Coroutineがどのような順番で呼ばれて欲しいのかに合わせて使い分ければよい。
// TODO
部分を埋めてテストを完成させよう。
- テストメソッド:
getVideoNewsResourcesStream_Flowを継続的にcollectするテストを書く
- テスト概要:
getVideoNewsResources
を呼び出すとvideoNewsStream
に更新が通知されることを、Flowをcollectすることで検証する
TestDispatcherについての理解を深めるために、いくつかのトピックを紹介する。
TestDispatcherはテストコード内で複数作成できるが、TestSchedulerは1つのインスタンスを共有する必要がある。
それはTestSchedulerが、テスト内で実行されるCoroutineの管理と仮想時間の制御をしているためである。
@Test
fun getVideoNewsResources() = runTest {
// runTest内で生成するTestDispatcherのコンストラクタにtestSchedulerを渡す
// testSchedulerはTestScopeのフィールドで、型はCoroutineTestScheduler
val job = launch(UnconfinedTestDispatcher(testScheduler)) {
..
}
}
StandardTestDispatcherで新しいCoroutineを開始すると、キューに追加されすぐには実行されない。advanceUntilIdleやrunCurrentといったヘルパーを使うと、任意のタイミングでキューに追加されたCoroutineを実行できる。
次のようにアサーションの前にadvanceUntilIdleを入れることで、StandardTestDispatcherを使用している場合でもテストが成功するようになる。
@Test
fun getVideoNewsResources() = runTest {
// ① テストコードセットアップ
val job = launch { // StandardTestDispatcherなのでコルーチンはキューに追加されて待機
// ③ videoNewsStreamのcollectのセットアップ
videoNewsRepository.videoNewsStream.toCollection(actual)
}
// ② 待機中のコルーチンを実行
advanceUntilIdle() // or runCurrent
// ④ テスト対象メソッドの実行 + アサーション
videoNewsRepository.refreshVideoNews()
}
Dispatchers.IO
やDispatchers.Default
でCoroutineを複数起動した場合、Coroutineの実行順序は保証されない。
次のコードを実行したときにtestとtest2のどちらが先にprintされるかは、実行するまでわからない。
@Test
fun test() = runTest {
launch(Dispatchers.Default) {
println("test")
}
launch(Dispatchers.Default) {
println("test2")
}
}
一方、TestDispatcherで開始されたCoroutineは、1つのテストスレッドで同期的に実行される。
Coroutineはキューに追加した順番で実行され、test → test2の順にprintされる。
@Test
fun test() = runTest {
// 明示的に指定しているが未指定の場合も同様
launch(StandardTestDispatcher(testScheduler)) {
println("test") // ①
}
launch(StandardTestDispatcher(testScheduler)) {
println("test2") // ②
}
advanceUntilIdle()
}
runTestに直接UnconfinedTestDispatcherを指定すると、runTestブロック内はデフォルトでUnconfinedTestDispatcherが使用される。
@Test
fun test() = runTest(UnconfinedTestDispatcher()) {
val job = launch {
// UnconfinedTestDispatcherで実行されるため、すぐにlaunch内の処理が行われる
}
..
}
データソースがテストデータを返すタイミングでdelayを差し込めるようできれば、通信処理に時間がかかっているときの振る舞いをシミュレートできる。 たとえば、エンドポイントAへのリクエストが2回連続で呼ばれたときに、1回目がリクエスト中だったら2回目のリクエストは無視するといった振る舞いのテストを実装できるようになる。
downloadVideo
メソッドでは、同じURLのリクエストが2回行われたときに、1回目のダウンロードに時間がかかっていれば2回目をスキップする。
suspend fun downloadVideo(url: String) {
// ダウンロード中のURLのSetをフィールドに保持
// ダウンロード中のだったらreturnして終了
if (downloadingUrlSet.contains(url)) {
return
}
// ダウンロード開始時にダウンロード先のURLをSetに追加
downloadingUrlSet.add(url)
// ダウンロード実行
// このときに時間がかかっている状態をdelayを使ってエミュレートする
networkDataSource.downloadVideoResource(url)
// ダウンロードが終了したらダウンロード先のURLをSetから削除
downloadingUrlSet.remove(url)
}
2回目がスキップされることを確認するテストは次のように書くことができる。
@Test
fun downloadVideo() = runTest {
// TestNetworkDataSourceでdownloadVideo実行時にdelayするように処理を差し込む
// また、NetworkDataSourceへの呼び出しをカウントして、1度しかダウンロードが実行されていないことを確認する
var downloadCount = 0
val testNetworkDataSource = TestNetworkDataSource(downloadVideoResourceFunc = {
delay(100)
downloadCount++
"test".toResponseBody()
})
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
// 1回目と2回目のリクエストをそれぞれ新しいコルーチンで実行する
launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.downloadVideo("https://host/movie.mp4")
}
launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.downloadVideo("https://host/movie.mp4")
}
// delay分の時間をすすめる
advanceUntilIdle()
// ダウンロードの実行が1回目しか行われていないことを確認する
Truth.assertThat(downloadCount).isEqualTo(1)
}
delayを入れたテストについて、いくつかのポイントを説明する。
runTest内で直接downloadVideo
を2回呼ぶと、1回目の実行が完了した後に2回目実行されるという処理の流れになる。今回は1回目がdelayしている間に2回目のリクエストをするといった流れにしたいため、それぞれ別のCoroutineを起動する。
それぞれ別のCoroutineにすることで、具体的には次の処理の流れになる。
- 1つめのCoroutineが起動し、1回目の
downloadVideo
が実行される。ダウンロード先のURLがURLのセットに含まれていないのでスキップせずに、URLをSetに追加する - 1回目の
downloadVideo
がNetworkDataSource#downloadVideoResource
を呼び出す。実態はTestNetworkDataSourceで、delay
が呼ばれ、一度処理を中断する - 2つめのCoroutineが起動し、2回目の
downloadVideo
が実行される。ダウンロード先のURLがURLのセットに含まれているため、早期returnで終了する - advanceUntilIdleで実行待ちのCoroutineがなくなるまで時間を進める
- 1回目の
downloadVideo
のdelayの待機が終了し、downloadCountがインクリメントされる。1回目のdownloadVideo
が終了する - downloadCountは1であることがアサートされる
TestScopeの中では、時間を操作する次のメソッドを使うことができる。
- advanceUntilIdle
- キューに何も残らなくなるまで、スケジュールされたCoroutineの実行を繰り返す
- 細かい時刻経過のコントロールが不要な場合は、advanceUntilIdleを使うのが楽
- advanceTimeBy
- 仮想時間を指定の分だけ進め、その時点までにスケジュール設定されているすべてのCoroutineを実行する
- runCurrent
- 現在の仮想時間にスケジュールされたCoroutineを実行する
さきほどのテストは、advanceTimeByに書きかえても成功する。advanceTimeByではdelay分の時間 + 1をした値を渡すと、実行まで進む。
@Test
fun downloadVideo() = runTest {
var downloadCount = 0
val testNetworkDataSource = TestNetworkDataSource(downloadVideoResourceFunc = {
delay(100)
downloadCount++
"test".toResponseBody()
})
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.downloadVideo("https://host/movie.mp4")
}
launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.downloadVideo("https://host/movie.mp4")
}
advanceTimeBy(101L) // advanceUntilIdleをadvanceTimeByに書き換え
Truth.assertThat(downloadCount).isEqualTo(1)
}
一方、runCurrentへの書き換えはテストが失敗する。runCurrentではdelay分の時間は進まないため、downloadCountが0のままテストが失敗する。
@Test
fun downloadVideo() = runTest {
var downloadCount = 0
val testNetworkDataSource = TestNetworkDataSource(downloadVideoResourceFunc = {
delay(100)
downloadCount++
"test".toResponseBody()
})
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.downloadVideo("https://host/movie.mp4")
}
launch(UnconfinedTestDispatcher(testScheduler)) {
videoNewsRepository.downloadVideo("https://host/movie.mp4")
}
runCurrent() // runCurrentに置き換え
Truth.assertThat(downloadCount).isEqualTo(1) // downloadCountは0のままで失敗
}
// TODO
部分を埋めてテストを完成させよう。
- テストメソッド:
downloadVideo_delayを入れたテストを書く
- テスト概要:
downloadVideo
を呼び出した後、ダウンロードが終わらないうちに同じURLを引数でdownloadVideo
を呼び出すと、2回目はスキップされることを確認する
runTestはDispatcherが切り替わっていた場合も戻り値が返ってくるまで待ってくれる。そのため、基本的にはrunTestで囲むだけでテストが成功する。
class VideoNewsRepository() {
// IOディスパッチャを直接指定
suspend fun getVideoNewsResources(): List<VideoNewsResource> = withContext(Dispatchers.IO){
return listOf()
}
}
@Test
fun getVideoNewsResources() = runTest {
val videoNewsRepository = DefaultVideoNewsRepository()
// Dispatcherが切り替わっていた場合も、戻り値が返ってくるまで待ってくれる
val actual = videoNewsRepository.getVideoNewsResources()
assertThat(actual).isEqualTo(expected)
}
ただし、「delayを入れたテストを書く」で紹介した実装にDispatchers.IO
を直接指定すると、成功していたテストが失敗してしまう。
// IOディスパッチャを直接指定
suspend fun downloadVideo(url: String) = withContext(Dispatchers.IO) {
// 省略
}
@Test
fun downloadVideo() = runTest {
var downloadCount = 0
val testNetworkDataSource = TestNetworkDataSource(downloadVideoResourceFunc = {
delay(1000)
downloadCount++
"test".toResponseBody()
})
val videoNewsRepository = DefaultVideoNewsRepository(testNetworkDataSource)
launch { videoNewsRepository.downloadVideo("https://host/movie.mp4") }
advanceUntilIdle()
Truth.assertThat(downloadCount).isEqualTo(1) // downloadCountは0のままで失敗
}
仮想時間やadvanceUntilIdleといったヘルパーを使うためには、TestSchedulerを共有したTestDispatcherを使う必要がある。 IO Dispatcherを直接指定していると、delayで一度中断したCoroutineを再開するために実時間を1秒進める必要があり、テストの実行時間が伸びてしまう。
Now in Android Appでは、Dagger Hiltを使ってIO DispatcherをコンストラクタからInjectできるようにしている。
// Qualifierの定義
@Qualifier
@Retention(RUNTIME)
annotation class Dispatcher(val niaDispatcher: NiaDispatchers)
enum class NiaDispatchers {
IO
}
// Moduleの定義
@Module
@InstallIn(SingletonComponent::class)
object DispatchersModule {
@Provides
@Dispatcher(IO)
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
}
// リポジトリの実装ではIO Dispatcherをコンストラクタから差し替えられるようにする
class DefaultVideoNewsRepository(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
) {
suspend fun downloadVideo(url: String) = withContext(ioDispatcher) {
}
}
テストコードでは、TestDispatcherに差し替えることで仮想時間が扱えるようになる。
@Test
fun `downloadVideo`() = runTest {
..
// TestDispatcherをコンスタントで渡すようにする
// runTest内でtestSchedulerを作成するときは、testSchedulerを共有する
val videoNewsRepository = DefaultVideoNewsRepository(
StandardTestDispatcher(testScheduler)
)
..
}
// TODO
部分を埋めてテストを完成させよう。
- テストメソッド:
downloadVideo_Dispatcherが指定されたテストを書く
- テスト概要:
downloadVideo2
を呼び出した後、ダウンロードが終わらないうちに同じURLを引数でdownloadVideo2
を呼び出すと、2回目はスキップされることを確認する - IO Dispatcherを外から差し込めるように DefaultVideoNewsRepositoryを修正する
API通信をするRepositoryを題材に、Kotlin Coroutine及びKotlin Flowのテストの書き方を紹介した。
- suspend関数をテストするには、runTestでテストメソッドを囲む
- FlowはFlow#firstといったFlowのAPIを使ってテストができる
- Flowをcollectする場合は、runTest内で新しいCoroutineを起動する
- delayとadvanceUntilIdleといった時間を操作するヘルパーを使うことで、Coroutineの中断や再開をコントロールできる
- Dispatcherを指定するときは、外から渡せるようにするとテストコードからのコントロールができるようになる