見出し画像

Turbineを用いてKotlin Flowのテストをする

はじめまして!note初投稿になります。
普段はAndroidエンジニアをしており、KotlinやJavaで地図アプリを作っています。
本日はKotlin Flowのテスト時に便利な「Turbine」というライブラリの紹介をいたします!

Turbineについて

Kotlin Flowについて調べていると、Andoridの公式ドキュメントから「Flowを簡単にテストできるサードパーティ製のライブラリ」ということでTurbineが紹介されておりました。
サードパーティ製とはいえ公式が推奨しているなら利用しても良いんじゃないかということで、調査してみることに。
なお、TurbineのガイドはgithubのREADMEに記述されております。

基本的な使い方

まずは、以下のような単純に文字列をemitするだけのflowを考えます。

	// プロダクションコード
	// 「SampleFlow」というクラスに定義されているメソッド
	fun produceData() : Flow<String> = flow{
		emit("aaa")
		emit("bbb")
	}

このコードのテストをTurbineを利用して記述すると以下のようになります。

	// テストコード
	@Test
	fun turbine_basic() = runTest{
		val sampleFlow = SampleFlow()
		sampleFlow.produceData().test {
			assertEquals("aaa"  , awaitItem())
			assertEquals("bbb"  , awaitItem())
			awaitComplete()
		}
	}

Flowの拡張関数の「test」

まずは、テストコードの以下記述の「test」について説明します。

	sampleFlow.produceData().test {

Turbine側で定義しているflowの拡張関数で、FlowからTurbineを作成して実行することができます。
これにより、新しいCoroutineを起動して、flowのcollectを起動しています。
つまり、testの中ではflowでemitされたデータを受け取ることが可能になります。

awaitItem()でemitされるデータを一つずつ受け取る

上述の「test」によってflowをcollectできるようになりました。
テストにおいては、「emitされたデータが一致しているか」というテストケースを作成することが多いと思います。
そんな時に役立つのがawaitItemになります。

	assertEquals("aaa"  , awaitItem())
	assertEquals("bbb"  , awaitItem())

この処理によって、emitされる文字列を順に取得してテストすることができます

awaitComplete()でemitが終了したことを確認

flowからそれ以上データがemitされないことを確認する方法もあります。
awaitComplete()では、flowが例外なく完了することを確認することができます。
例えば、テストコードを以下のように書き換えたとします。("bbb"の収集をテストしない)

	sampleFlow.produceData().test {
		assertEquals("aaa"  , awaitItem())
		awaitComplete()

これでテストを実行すると、以下のエラーが発生します。
"bbb"が見つかったよ!と言われるわけですね。

	app.cash.turbine.TurbineAssertionError: Expected complete but found Item(bbb)

異常系の確認

次は異常系のテストについて説明します。
文字列を一度emitしたあと、意図的にExceptionを発生させています。

	// プロダクションコード
	// 「SampleFlow」というクラスに定義されているメソッド 
	fun produceData() : Flow<String> = flow{
		assertEquals("aaa"  , awaitItem())
		throw(RuntimeException("例外発生"))
	}

テストコードは以下のように記述できます。

	fun turbine_error() = runTest{
		val sampleFlow = SampleFlow()
		sampleFlow.produceData().test {
			assertEquals("aaa"  , awaitItem())
			assertEquals("例外発生"  , awaitError().message)
		}
	}

awaitError()でエラーイベントを収集

Flow内部で例外をスローしても、Turbineの中で例外がスローされる訳ではなく、
エラーイベントとして収集することができます。

複数のFlowを収集してテストする

二種類のFlowがある場合を考えます。

	class SampleFlow {
		fun produceData1() : Flow<String> = flow{
			emit("aaa")
			emit("bbb")
		}
		fun produceData2() : Flow<String> = flow{
			emit("ccc")
			emit("ddd")
		}
	}

(このようなテストケースはないと思いますが)
二種類のFlowを使って以下のようにテストを実装することができます。

	fun turbin_multi() = runTest{
		turbineScope{
			val sampleFlow = SampleFlow()
			val turbine1 = sampleFlow.produceData().testIn(backgroundScope)
			val turbine2 = sampleFlow.produceData().testIn(backgroundScope)
			assertEquals("aaa"  , turbine1.awaitItem())
			assertEquals("ccc"  , turbine2.awaitItem())
			assertEquals("bbb"  , turbine1.awaitItem())
			assertEquals("ddd"  , turbine2.awaitItem())
			turbine1.awaitComplete()
			turbine2.awaitComplete()
		}
	}

testInでTurbineを変数に保存

上述で紹介した方法では拡張関数testを利用していましたが、
代わりにtestInを利用するとTurbineを変数に保存することができます
これにより複数のTurbineを扱うことができます。

		val turbine1 = sampleFlow.produceData().testIn(backgroundScope)

testInの中で指定している「backgroundScope」はrunTest内で利用できる変数です。
testIn自体はCoroutineの自動クリーンアップができませんが、
CoroutineScopeにbackgroundScopeを指定することで、Coroutineのクリーンアップを自動的に行なってくれます。
※ 参考 http://y-anz-m.blogspot.com/2022/08/kotlin-coroutines-164.html

終わりに

Flowのテストが簡易化できるライブラリのTurbineについて紹介しました。
初投稿でしたが楽しく書けました!

この記事が気に入ったらサポートをしてみませんか?