Wie Sie Einheiten oder Module testen, die mit dem Ablauf kommunizieren, hängt davon ab, ob das zu testende Element den Ablauf als Eingabe oder Ausgabe verwendet.
- Wenn das zu testende Subjekt einen Ablauf beobachtet, können Sie Abläufe 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 im Test ein oder mehrere Elemente lesen und überprüfen, die von einem Flow ausgegeben werden.
Fake-Ersteller erstellen
Wenn das zu testende Subjekt ein Consumer eines Flows ist, besteht eine gängige Methode zum Testen darin, den Producer durch eine Fälschung zu ersetzen. Angenommen, Sie haben eine Klasse, die ein Repository überwacht, das in der Produktion Daten aus zwei Datenquellen abruft:
Um den Test deterministisch zu machen, 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 ausgeben möchten, verwenden Sie den Builder flow:
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 // ... }
Da Sie jetzt die Ausgaben des zu testenden Elements steuern können, können Sie anhand der Ausgaben prüfen, ob es richtig funktioniert.
Flow-Emissionen in einem Test bestätigen
Wenn das zu testende Subjekt einen Flow bereitstellt, müssen im Test Assertionen für die Elemente des Datenstreams vorgenommen werden.
Angenommen, im Repository des vorherigen Beispiels wird ein Flow bereitgestellt:
Bei bestimmten Tests müssen Sie nur die erste Emission oder eine begrenzte Anzahl von Elementen aus dem Flow prüfen.
Sie können die erste Emission des Flows 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 geprüft werden müssen, wird durch den Aufruf von toList() gewartet, bis die Quelle alle ihre Werte ausgegeben hat. Diese Werte werden dann als Liste zurückgegeben. Das 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) }
Bei Datenstreams, für die eine komplexere Erfassung 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 Erhebung während eines Tests
Wenn Sie einen Ablauf 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.
Wenn Sie Aktionen verschachteln möchten, die dazu führen, dass der Flow Werte und Zusicherungen zu den ausgegebenen Werten ausgibt, können Sie während eines Tests kontinuierlich Werte aus einem Flow erfassen.
Nehmen wir beispielsweise die folgende Repository-Klasse, die getestet werden soll, und eine zugehörige Implementierung einer gefälschten Datenquelle mit einer emit-Methode, um Werte während des Tests dynamisch 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 dieses Fake in einem Test verwenden, können Sie eine Sammel-Coroutine erstellen, die kontinuierlich die Werte aus Repository empfängt. In diesem Beispiel werden sie in einer Liste zusammengefasst und dann werden Zusicherungen für den Inhalt der Liste ausgeführt:
@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 Ablauf hier nie abgeschlossen wird, wird der toList-Aufruf, mit dem er erfasst wird, nie zurückgegeben. Wenn Sie die Collecting-Coroutine in TestScope.backgroundScope starten, wird sie vor dem Ende des Tests abgebrochen. Andernfalls würde runTest weiterhin 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 Collecting-Coroutine verwendet wird. Dadurch wird sichergestellt, dass die Collecting-Coroutine sofort gestartet wird und bereit ist, Werte zu empfangen, nachdem launch zurückgegeben wurde.
Turbine verwenden
Die Drittanbieterbibliothek Turbine bietet eine praktische API zum Erstellen einer Collecting-Coroutine 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.
StateFlows testen
StateFlow ist ein beobachtbarer Daten-Holder, der erfasst werden kann, um die darin enthaltenen Werte im Zeitverlauf als Stream zu beobachten. Beachten Sie, dass dieser Stream von Werten zusammengefasst wird. Wenn Werte in einem StateFlow schnell festgelegt werden, erhalten die Empfänger dieses StateFlow nicht garantiert alle Zwischenwerte, sondern nur den letzten.
Wenn Sie bei Tests die Konfundierung berücksichtigen, können Sie die Werte von StateFlow wie bei jedem anderen Ablauf erfassen, auch mit Turbine. In einigen Testszenarien kann es wünschenswert sein, alle Zwischenwerte zu erfassen und zu bestätigen.
Wir empfehlen jedoch im Allgemeinen, StateFlow als Dateninhaber zu behandeln und stattdessen die value-Property zu bestätigen. So wird der aktuelle Status des Objekts zu einem bestimmten Zeitpunkt validiert. Es spielt keine Rolle, ob eine Zusammenführung stattfindet.
Hier ist ein Beispiel für ViewModel, das Werte aus einem Repository erfasst und sie in einer StateFlow in der Benutzeroberfläche verfügbar macht:
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 ViewModel mit diesem Fake testen, können Sie Werte aus dem Fake ausgeben, um Aktualisierungen in der StateFlow von ViewModel auszulösen, und dann den aktualisierten value bestätigen:
@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 im ViewModel ein MutableStateFlow verwendet, um den letzten Wert zu speichern, der von einem Flow aus dem Repository ausgegeben wird. Dies ist ein häufiges Muster, das in der Regel einfacher mit dem Operator stateIn implementiert wird, der einen Cold Flow in einen Hot Flow vom Typ StateFlow konvertiert:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() { val score: StateFlow<Int> = myRepository.scores() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0) }
Der Operator stateIn hat einen SharingStarted-Parameter, der bestimmt, wann er aktiv wird und mit der Verarbeitung des zugrunde liegenden Streams beginnt. Optionen wie SharingStarted.Lazily und SharingStarted.WhileSubscribed werden häufig in Ansichtsmodellen verwendet.
stateIn
Auch wenn Sie in Ihrem Test den value des StateFlow bestätigen, 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-Koroutinen unter Android testen
- Kotlin-Flows unter Android
StateFlowundSharedFlow- Coroutines testen