Android での Kotlin Flow のテスト

Flow を使用するユニットやモジュールのテスト方法は、テスト対象が Flow を入力として使用するか、出力として使用するかによって異なります。

  • テスト対象から Flow を監視する場合は、制御可能な疑似依存関係内で Flow を生成するという方法があります。
  • ユニットやモジュールで Flow を公開している場合は、テストで Flow から出力されるアイテムの読み取りや確認を行えます。

疑似プロデューサの作成

テスト対象が Flow のコンシューマである場合、よく使われるのはプロデューサを疑似実装に置き換えるという方法です。たとえば、本番環境の 2 つのデータソースからデータを取得しているリポジトリを監視するクラスがあるとします。

テスト対象とデータレイヤ
図 1. テスト対象とデータレイヤ

リポジトリとその依存関係を、常に同じ疑似データを出力する疑似リポジトリに置き換えることで、テストの条件を固定化できます。

依存関係を疑似実装に置き換える
図 2. 依存関係を疑似実装に置き換える

事前に定義した一連の値を Flow に出力するには、次のように flow ビルダーを使用します。

class MyFakeRepository : MyRepository {
    fun observeCount() = flow {
        emit(ITEM_1)
    }
}

テストでは、次のようにして疑似リポジトリを挿入し、実際の実装を置き換えます。

@Test
fun myTest() {
    // Given a class with fake dependencies:
    val sut = MyUnitUnderTest(MyFakeRepository())
    // Trigger and verify
    ...
}

これで、テスト対象への出力を制御できるようになったため、そこからの出力をチェックすることで正しく機能しているかを確認できます。

テストにおける Flow 出力のアサーション

テスト対象で Flow を公開している場合、テストではデータ ストリームの要素についてアサーションを行う必要があります。

上記の例のリポジトリで Flow を公開しているとします。

Flow を公開している疑似依存関係を含むリポジトリ
図 3. Flow を公開している疑似依存関係を含むリポジトリ(テスト対象)

テストでの必要性に応じて、通常は Flow からの最初の出力、または有限個のアイテムをチェックします。

first() を呼び出すことで、Flow への最初の出力を使用できます。この関数は、最初のアイテムを受け取るまで待機し、その後キャンセル信号をプロデューサに送信します。

@Test
fun myRepositoryTest() = runBlocking {
    // Given a repository that combines values from two data sources:
    val repository = MyRepository(fakeSource1, fakeSource2)

    // When the repository emits a value
    val firstItem = repository.counter.first() // Returns the first item in the flow

    // Then check it's the expected item
    assertThat(firstItem, isEqualTo(ITEM_1) // Using AssertJ
}

テストで複数の値をチェックする必要がある場合は、toList() を呼び出します。これにより、Flow はソースからすべての値が出力されるのを待ってから、一連の値をリストとして返します。この関数は、有限個のデータ ストリームに対してのみ有効です。

@Test
fun myRepositoryTest() = runBlocking {
    // Given a repository with a fake data source that emits ALL_MESSAGES
    val messages = repository.observeChatMessages().toList()

    // When all messages are emitted then they should be ALL_MESSAGES
    assertThat(messages, isEqualTo(ALL_MESSAGES))
}

より複雑なアイテムのコレクションを必要とするデータ ストリームや、有限個のアイテムを返さないデータ ストリームの場合は、Flow API を使用することでアイテムの選択および変換を行えます。次に例を示します。

// Take the second item
outputFlow.drop(1).first()

// Take the first 5 items
outputFlow.take(5).toList()

// Take the first 5 distinct items
outputFlow.take(5).toSet()

// Take the first 2 items matching a predicate
outputFlow.takeWhile(predicate).take(2).toList()

// Take the first item that matches the predicate
outputFlow.firstWhile(predicate)

// Take 5 items and apply a transformation to each
outputFlow.map(transformation).take(5)

// Takes the first item verifying that the flow is closed after that
outputFlow.single()

// Finite data streams
// Verify that the flow emits exactly N elements (optional predicate)
outputFlow.count()
outputFlow.count(predicate)

依存関係としての CoroutineDispatcher

テスト対象が CoroutineDispatcher を依存関係として使用する場合は、kotlinx-coroutines-test ライブラリTestCoroutineDispatcher のインスタンスを渡し、そのディスパッチャの runBlockingTest メソッド内でテスト本体を実行します。

private val coroutineDispatcher = TestCoroutineDispatcher()
private val uut = MyUnitUnderTest(coroutineDispatcher)

@Test
fun myTest() = coroutineDispatcher.runBlockingTest {
    // Test body
}

Flow に関する参考情報