在 Android 上測試 Kotlin 資料流

測試與資料流通訊的單元或模組的方式,取決於接受測試的主體是否將資料流做為輸入內容或輸出內容。

  • 如果受測試的主體觀測資料流,則能在可透過測試控制的假依附元件內產生資料流。
  • 如果單元或模組會公開資料流,您就可以讀取和驗證測試中的資料流發出的一個或多個項目。

建立虛假生產者

如果受測試的主體是資料流的消費者,一種常見的測試方式是,以假的實作取代生產者。例如,假設某個類別會觀測存放區,而該存放區會從生產環節中的兩個資料來源擷取資料:

受測試的主體和資料層
圖 1. 受測試的主體和資料層。

如要使測試具有確定性,您可以將存放區及其依附元件替換成一直發出相同假資料的假存放區:

以假實作取代依附元件
圖 2. 以假實作取代依附元件。

如要發出資料流中一系列預先定義的值,請使用 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
    ...
}

現在,您已控制受測試主體的輸出內容,所以可以透過檢查輸出內容,來驗證主體是否正常執行。

在測試中聲明資料流發出作業

如果受測試的主體在公開資料流,測試需要針對資料串流元素發表斷言。

假設上一個範例的存放區公開資料流:

公開資料流且包含假依附元件的存放區
圖 3. 公開資料流且包含假依附元件的存放區 (受測試的主體)。

視測試的需要而定,您通常會查看第一個發出作業或來自資料流的有限數量的項目

只要呼叫 first(),即可取用向資料流發出的第一個發出作業。此函式會等待接收第一個項目,然後將取消訊號傳送給生產者。

@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() 會讓資料流等待來源發出其所有值,然後再將這些值傳回,做為清單。請注意,這項功能僅適用於有限的資料串流。

@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 資料庫提供的測試調派程式,在測試中取代這些調派程式:StandardTestDispatcherUnconfinedTestDispatcher

runTest 中,請使用提供的 testScheduler 建立新的測試調派程式,然後將調派程式傳遞給主體:

@Test
fun myTest() = runTest {
    val uut = MyUnitUnderTest(UnconfinedTestDispatcher(testScheduler))
    // Test body
}

其他資料流資源