Wie Sie Einheiten oder Module testen, die mit einem Flow kommunizieren, hängt davon ab, ob das Testobjekt den Flow als Eingabe oder Ausgabe verwendet.
- Wenn das Testobjekt einen Flow beobachtet, können Sie Flows in gefälschten Abhängigkeiten generieren, die Sie über Tests steuern können.
- Wenn die Einheit oder das Modul einen Flow bereitstellt, können Sie in einem Test ein oder mehrere Elemente lesen und überprüfen, die von einem Flow ausgegeben werden.
Gefälschten Producer erstellen
Wenn das Testobjekt ein Consumer eines Flows ist, besteht eine gängige Methode zum Testen darin, den Producer durch eine gefälschte Implementierung zu ersetzen. Angenommen, es gibt eine Klasse, die ein Repository beobachtet, das Daten aus zwei Datenquellen in der Produktion abruft:
Damit der Test deterministisch ist, können Sie das Repository und seine Abhängigkeiten durch ein gefälschtes Repository ersetzen, das immer dieselben gefälschten Daten ausgibt:
Verwenden Sie den flow-Builder, um eine vordefinierte Reihe von Werten in einem Flow auszugeben:
class MyFakeRepository : MyRepository { fun observeCount() = flow { emit(ITEM_1) } }
Im Test wird dieses gefälschte Repository eingefügt und ersetzt die tatsächliche Implementierung:
@Test fun myTest() { // Given a class with fake dependencies: val sut = MyUnitUnderTest(MyFakeRepository()) // Trigger and verify // ... }
Jetzt haben Sie die Kontrolle über die Ausgaben des Testobjekts und können anhand der Ausgaben überprüfen, ob es richtig funktioniert.
Flow-Ausgaben in einem Test bestätigen
Wenn das Testobjekt einen Flow bereitstellt, müssen im Test Zusicherungen zu den Elementen des Datenstreams gemacht werden.
Angenommen, das Repository aus dem vorherigen Beispiel stellt einen Flow bereit:
Bei bestimmten Tests müssen Sie nur die erste Ausgabe oder eine begrenzte Anzahl von Elementen aus dem Flow überprüfen.
Sie können die erste Ausgabe an den Flow mit first() abrufen. Diese Funktion wartet, bis das erste Element empfangen wurde, und sendet dann das Abbruchsignal an den Producer.
@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 überprüft werden müssen, wartet toList() darauf, dass die Quelle alle Werte ausgibt, und gibt diese Werte dann als Liste zurück. Dies funktioniert nur bei endlichen Datenstreams.
@Test fun myRepositoryTestList() = runTest { val repository = MyFakeRepository() // 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) }
Für Datenstreams, die eine komplexere Sammlung von Elementen erfordern oder keine endliche Anzahl von Elementen zurückgeben, können Sie mit der Flow API Elemente auswählen und 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 Flow mit toList() erfassen, wie im vorherigen Beispiel gezeigt, wird intern collect() verwendet und der Vorgang wird angehalten, bis die gesamte Ergebnisliste zurückgegeben werden kann.
Um Aktionen zu verschachteln, die dazu führen, dass der Flow Werte ausgibt, und Zusicherungen zu den ausgegebenen Werten zu machen, können Sie während eines Tests kontinuierlich Werte aus einem Flow erfassen.
Nehmen wir zum Beispiel die folgende Repository-Klasse, die getestet werden soll, und eine begleitende gefälschte Datenquellenimplementierung mit einer emit-Methode, um während des Tests dynamisch Werte zu erzeugen:
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 diese Fälschung in einem Test verwenden, können Sie eine Coroutine zum Erfassen erstellen, die kontinuierlich die Werte aus dem Repository empfängt. In diesem Beispiel werden sie in einer Liste erfasst und dann werden Zusicherungen zu ihrem Inhalt gemacht:
@OptIn(ExperimentalCoroutinesApi::class) @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 von Repository bereitgestellte Flow hier nie abgeschlossen wird, wird der toList-Aufruf, der ihn erfasst, nie zurückgegeben. Wenn Sie die Coroutine zum Erfassen in
TestScope.backgroundScope
starten, wird sie vor dem Ende des Tests abgebrochen. Andernfalls wartet runTest weiter auf den Abschluss, wodurch der Test nicht mehr reagiert und schließlich fehlschlägt.
Beachten Sie, dass hier
UnconfinedTestDispatcher
für die Coroutine zum Erfassen verwendet wird. Dadurch wird sichergestellt, dass die Coroutine zum Erfassen sofort gestartet wird und nach der Rückgabe von launch Werte empfangen kann.
Turbine verwenden
Die Turbine Bibliothek eines Drittanbieters bietet eine praktische API zum Erstellen einer Coroutine zum Erfassen sowie weitere praktische Funktionen zum Testen von Flows:
@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 für mehr Details.
StateFlows testen
StateFlow ist ein beobachtbarer Datenhalter, der erfasst werden kann, um die darin enthaltenen Werte im Zeitverlauf als Stream zu beobachten. Dieser Stream von Werten wird zusammengeführt. Das bedeutet, dass Collector eines StateFlow nicht garantiert alle Zwischenwerte, sondern nur den letzten Wert erhalten, wenn Werte schnell in einem StateFlow festgelegt werden.
Wenn Sie die Zusammenführung im Hinterkopf behalten, können Sie in Tests die Werte eines StateFlow wie jeden anderen Flow erfassen, auch mit Turbine. In einigen Testszenarien kann es wünschenswert sein, alle Zwischenwerte zu erfassen und Zusicherungen zu machen.
Im Allgemeinen empfehlen wir jedoch, StateFlow als Datenhalter zu behandeln und stattdessen Zusicherungen zu seiner value-Property zu machen. So werden im Test der aktuelle Status des Objekts zu einem bestimmten Zeitpunkt validiert und es hängt nicht davon ab, ob eine Zusammenführung erfolgt.
Nehmen wir zum Beispiel dieses ViewModel, das Werte aus einem Repository erfasst und sie in einem StateFlow für die UI bereitstellt:
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 dieses 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 das ViewModel mit dieser Fälschung testen, können Sie Werte aus der Fälschung ausgeben, um Aktualisierungen im StateFlow des ViewModel auszulösen, und dann Zusicherungen zum aktualisierten value machen:
@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 verwendet das ViewModel ein MutableStateFlow, um den letzten Wert zu speichern, der von einem Flow aus dem Repository ausgegeben wurde. Dies ist ein gängiges Muster,
das in der Regel einfacher mit dem
stateIn
Operator implementiert wird, der einen Cold Flow in einen Hot 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 bestimmt, wann er aktiv wird und den zugrunde liegenden Flow verwendet. Optionen wie SharingStarted.Lazily und SharingStarted.WhileSubscribed werden häufig in Viewmodels verwendet.
Auch wenn Sie im Test Zusicherungen zum value des StateFlow machen, müssen Sie einen Collector erstellen. Das kann ein leerer Collector sein:
@OptIn(ExperimentalCoroutinesApi::class) @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) }
Zusätzliche Ressourcen
- Kotlin-Coroutinen unter Android testen
- Kotlin-Flows unter Android
StateFlowundSharedFlow- Zusätzliche Ressourcen für Kotlin-Coroutinen und -Flows