Die Art und Weise, wie Sie Einheiten oder Module testen, die mit flow kommunizieren, hängt davon ab, ob das Testobjekt den Ablauf als Eingabe oder Ausgabe verwendet.
- Wenn die Testperson einen Ablauf beobachtet, können Sie Abläufe innerhalb von gefälschten Abhängigkeiten generieren, die Sie über Tests steuern können.
- Wenn die Einheit oder das Modul einen Ablauf bereitstellt, können Sie ein oder mehrere Elemente lesen und überprüfen, die von einem Ablauf im Test gesendet wurden.
Erstellen eines gefälschten Erstellers
Wenn das Testobjekt ein Verbraucher eines Ablaufs ist, wird es häufig durch Ersetzen des Erzeugers durch eine gefälschte Implementierung getestet. Angenommen, es gibt eine Klasse, die ein Repository beobachtet, das Daten aus zwei Datenquellen in der Produktion bezieht:
Um den Test deterministisch zu gestalten, können Sie das Repository und seine Abhängigkeiten durch ein gefälschtes Repository ersetzen, das immer dieselben gefälschten Daten ausgibt:
Wenn Sie eine vordefinierte Reihe von Werten in einem Ablauf senden möchten, verwenden Sie den flow
-Builder:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
Im Test wird dieses gefälschte Repository eingefügt und ersetzt die echte Implementierung:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Da Sie jetzt die Kontrolle über die Ausgaben des Testobjekts haben, können Sie prüfen, ob es richtig funktioniert.
Flussemissionen in einem Test prüfen
Wenn das Testobjekt einen Datenfluss sendet, müssen im Test Behauptungen zu den Elementen des Datenstreams aufgestellt werden.
Angenommen, das Repository des vorherigen Beispiels stellt einen Ablauf bereit:
Bei bestimmten Tests müssen Sie nur die erste Auslieferung oder eine bestimmte Anzahl von Elementen aus dem Datenfluss prüfen.
Du kannst die erste Emission für den Ablauf verbrauchen, indem du first()
aufrufst. Diese Funktion wartet, bis der erste Artikel empfangen wurde, und sendet dann das Stornierungssignal an den Produzenten.
@Test
fun myRepositoryTest() = runTest {
// 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
assertEquals(ITEM_1, firstItem)
}
Wenn im Test mehrere Werte geprüft werden müssen, wartet der Ablauf beim Aufrufen von toList()
, bis die Quelle alle Werte ausgegeben hat, und gibt diese dann als Liste zurück. Dies funktioniert nur für endliche Datenstreams.
@Test
fun myRepositoryTest() = runTest {
// 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
assertEquals(ALL_MESSAGES, messages)
}
Bei Datenstreams, für die eine komplexere Sammlung von Elementen erforderlich ist oder die keine endliche Anzahl von Elementen zurückgeben, können Sie die Flow
API verwenden, um Elemente auszuwählen und zu transformieren. Hier einige Beispiele:
// Take the second item
outputFlow.drop(1).first()
// Take the first 5 items
outputFlow.take(5).toList()
// 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)
Kontinuierliche Erfassung während eines Tests
Wenn Sie einen Fluss wie im vorherigen Beispiel mit toList()
erfassen, wird collect()
intern verwendet und der Vorgang wird pausiert, bis die gesamte Ergebnisliste zurückgegeben werden kann.
Wenn Sie Aktionen einfügen möchten, die dazu führen, dass der Ablauf Werte ausgibt, und Behauptungen zu den ausgegeben Werten, können Sie während eines Tests kontinuierlich Werte aus einem Ablauf erfassen.
Nehmen wir als Beispiel die folgende zu testende Repository
-Klasse und eine zugehörige Implementierung einer gefälschten Datenquelle mit einer emit
-Methode, um während des Tests dynamisch Werte zu generieren:
class Repository(private val dataSource: DataSource) {
fun scores(): Flow<Int> {
return dataSource.counts().map { it * 10 }
}
}
class FakeDataSource : DataSource {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun counts(): Flow<Int> = flow
}
Wenn Sie diesen Fake in einem Test verwenden, können Sie eine erfassende Coroutine erstellen, die kontinuierlich die Werte aus der Repository
empfängt. In diesem Beispiel werden sie in einer Liste gesammelt und dann werden Behauptungen über den Inhalt aufgestellt:
@Test
fun continuouslyCollect() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
repository.scores().toList(values)
}
dataSource.emit(1)
assertEquals(10, values[0]) // Assert on the list contents
dataSource.emit(2)
dataSource.emit(3)
assertEquals(30, values[2])
assertEquals(3, values.size) // Assert the number of items collected
}
Da der durch Repository
freigegebene Fluss hier nie abgeschlossen wird, wird der toList
-Aufruf, der ihn erfasst, nie zurückgegeben. Wenn Sie die erfassende Coroutine in TestScope.backgroundScope
starten, wird sie vor dem Ende des Tests abgebrochen. Andernfalls würde runTest
weiter auf den Abschluss warten, was dazu führen würde, dass der Test nicht mehr reagiert und schließlich fehlschlägt.
Beachten Sie, dass hier UnconfinedTestDispatcher
für die erfassende Coroutine verwendet wird. So wird sichergestellt, dass die erfassende Coroutine sofort gestartet wird und nach der Rückgabe von launch
bereit ist, Werte zu empfangen.
Turbine verwenden
Die Drittanbieterbibliothek Turbine bietet eine praktische API zum Erstellen einer sammelnden Coroutine sowie weitere praktische Funktionen zum Testen von Abläufen:
@Test
fun usingTurbine() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
repository.scores().test {
// Make calls that will trigger value changes only within test{}
dataSource.emit(1)
assertEquals(10, awaitItem())
dataSource.emit(2)
awaitItem() // Ignore items if needed, can also use skip(n)
dataSource.emit(3)
assertEquals(30, awaitItem())
}
}
Weitere Informationen finden Sie in der Dokumentation der Bibliothek.
StateFlows testen
StateFlow
ist ein beobachtbarer Datenhalter, der erfasst werden kann, um die Werte im Zeitverlauf als Stream zu beobachten. Dieser Wertestream wird zusammengeführt. Wenn also Werte in einem StateFlow
schnell festgelegt werden, erhalten die Abholer dieses StateFlow
nicht garantiert alle Zwischenwerte, sondern nur den jeweils neuesten.
Wenn Sie bei Tests die Zusammenführung berücksichtigen, können Sie die Werte von StateFlow
wie bei jedem anderen Fluss erfassen, auch mit Turbine. In einigen Testszenarien kann es sinnvoll sein, alle Zwischenwerte zu erfassen und zu prüfen.
Wir empfehlen jedoch generell, StateFlow
als Dateninhaber zu behandeln und stattdessen einen Anspruch auf seine value
-Property geltend zu machen. So wird der aktuelle Status des Objekts zu einem bestimmten Zeitpunkt geprüft und die Tests sind unabhängig davon, ob eine Zusammenführung stattfindet oder nicht.
Hier ist beispielsweise eine ViewModel
zu sehen, die Werte aus einer Repository
erfasst und in einer StateFlow
in der Benutzeroberfläche anzeigt:
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()
fun initialize() {
viewModelScope.launch {
myRepository.scores().collect { score ->
_score.value = score
}
}
}
}
Eine gefälschte Implementierung für diese Repository
könnte so aussehen:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
Wenn Sie die ViewModel
mit diesem Fake testen, können Sie Werte aus dem Fake ausgeben, um Aktualisierungen im StateFlow
der ViewModel
auszulösen, und dann eine Behauptung für die aktualisierte value
aufstellen:
@Test
fun testHotFakeRepository() = runTest {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
assertEquals(0, viewModel.score.value) // Assert on the initial value
// Start collecting values from the Repository
viewModel.initialize()
// Then we can send in values one by one, which the ViewModel will collect
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value) // Assert on the latest value
}
Mit StateFlows arbeiten, die mit „stateIn“ erstellt wurden
Im vorherigen Abschnitt wird in der ViewModel
ein MutableStateFlow
verwendet, um den letzten Wert zu speichern, der von einem Fluss aus der Repository
gesendet wurde. Dies ist ein gängiges Muster, das in der Regel einfacher mit dem Operator stateIn
implementiert wird, der einen kalten Fluss in einen heißen StateFlow
umwandelt:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
Der Operator stateIn
hat einen Parameter SharingStarted
, der festlegt, wann er aktiv wird und mit der Verarbeitung des zugrunde liegenden Ablaufs beginnt. Optionen wie SharingStarted.Lazily
und SharingStarted.WhileSubscribed
werden häufig in Ansichtsmodellen verwendet.
Auch wenn Sie in Ihrem Test eine Assertion für die value
der StateFlow
ausführen, müssen Sie einen Collector erstellen. Dies kann ein leerer Collector sein:
@Test
fun testLazilySharingViewModel() = runTest {
val fakeRepository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(fakeRepository)
// Create an empty collector for the StateFlow
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.score.collect {}
}
assertEquals(0, viewModel.score.value) // Can assert initial value
// Trigger-assert like before
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value)
}
Weitere Informationen
- Kotlin-Coroutinen unter Android testen
- Kotlin-Abläufe unter Android
StateFlow
undSharedFlow
- Weitere Ressourcen zu Kotlin-Goroutinen und ‑Abläufen