Die Art und Weise, wie Sie Einheiten oder Module testen, die mit Ablauf kommunizieren hängt davon ab, ob die Testperson den Fluss als Eingabe oder Ausgabe verwendet.
- Wenn die Testperson einen Fluss beobachtet, kannst du Ströme innerhalb fiktive Abhängigkeiten, die Sie über Tests steuern können.
- Wenn die Einheit oder das Modul einen Ablauf anzeigt, können Sie einen oder die von einem Datenfluss im Test ausgegeben wurden.
fiktiven Producer erstellen
Wenn die Testperson ein Nutzer eines Datenflusses ist, ist eine gängige Methode zum Testen indem der Ersteller durch eine falsche Implementierung ersetzt wird. Wenn z. B. ein die ein Repository beobachtet, das Daten aus zwei Datenquellen Produktion:
<ph type="x-smartling-placeholder">Um den Test deterministisch zu machen, können Sie das Repository und seine Abhängigkeiten mit einem fiktiven Repository, das immer die gleichen fiktiven Daten ausgibt:
<ph type="x-smartling-placeholder">Verwenden Sie den flow
-Builder, um eine vordefinierte Reihe von Werten in einem Ablauf auszugeben:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
Im Test wird dieses fiktive Repository eingeschleust, das das echte Implementierung:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Jetzt, da Sie die Kontrolle über die Ausgaben der zu testenden Person haben, können Sie ob er ordnungsgemäß funktioniert, indem Sie seine Ausgaben überprüfen.
Strömungsemissionen in einem Test bestätigen
Wenn das Testsubjekt einen Ablauf anzeigt, muss der Test Assertions machen zu den Elementen des Datenstreams.
Nehmen wir an, dass das Repository des vorherigen Beispiels einen Ablauf anzeigt:
<ph type="x-smartling-placeholder">Bei bestimmten Tests müssen Sie nur die erste Emission oder eine endliche Anzahl der Elemente aus dem Ablauf.
Sie können die erste Emission für den Datenfluss verbrauchen, indem Sie first()
aufrufen. Dieses
wartet, bis der erste Artikel eingegangen ist, und sendet dann den
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 beim Test mehrere Werte geprüft werden müssen, führt der Aufruf von toList()
zum Ablauf
warten, bis die Quelle alle ihre Werte ausgibt und diese Werte dann als eine
Liste. Dies funktioniert nur für begrenzte 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)
}
Für Datenstreams, die eine komplexere Sammlung von Elementen erfordern oder bei denen keine
eine endliche Anzahl von Elementen haben, können Sie die Flow
API verwenden, um Elemente auszuwählen und
Elemente. 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
Beim Erfassen eines Ablaufs mit toList()
, wie im vorherigen Beispiel gezeigt,
collect()
und wird gesperrt, bis die gesamte Ergebnisliste bereit ist.
zurückgegeben.
Zum Verschachteln von Aktionen, die dazu führen, dass der Fluss Werte und Assertions für die ausgegeben wurden, können Sie während des gesamten Prozesses einen Test.
Nehmen wir zum Beispiel die folgende Repository
-Klasse zum Testen und einen
eine fiktive Datenquellen-Implementierung
mit einer emit
-Methode,
während des Tests dynamisch Werte 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 Erfassungskoroutine erstellen, die
erhält kontinuierlich die Werte von Repository
. In diesem Beispiel
sie in einer Liste zu sammeln und anschließend Assertions für deren Inhalt vorzunehmen:
@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
hier dargestellte Datenfluss nicht abgeschlossen wird, wird der toList
der sie erfasst,
nie wiederkehrt. Gemeinsame Routine für das Sammeln starten in
TestScope.backgroundScope
sorgt dafür, dass die Koroutine vor dem Ende des Tests abgebrochen wird. Andernfalls
runTest
würde weiter auf den Abschluss warten, wodurch der Test beendet wird.
und schließlich ausfallen.
Beachten Sie, wie
UnconfinedTestDispatcher
wird hier für die Erfassungskoroutine verwendet. So wird sichergestellt, dass die Erfassung
coroutine wird mit Spannung gestartet und kann nach dem launch
Werte empfangen
Rücksendungen.
Turbine verwenden
Die Turbine des Drittanbieters bietet eine praktische API zum Erstellen einer Erfassungskoroutine. 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 für erhalten Sie weitere Informationen.
StateFlows testen
StateFlow
ist ein beobachtbarer Wert
Dateninhaber, der erfasst werden kann, um die Werte, die er im Laufe der Zeit hat,
einem Stream. Dieser Wertestrom wird verkettet. Wenn also
werden die Werte schnell in einer StateFlow
festgelegt, die Collectors dieser StateFlow
erhält garantiert alle Zwischenwerte, nur den neuesten.
Wenn Sie die Konfusion in Tests beachten, können Sie die Werte einer StateFlow
erfassen.
da du auch andere Strömungen erfassen kannst, auch mit der Turbine. Es wird versucht, Daten zu erheben
und Assertions für alle Zwischenwerte können
in einigen Testszenarien sinnvoll sein.
Wir empfehlen jedoch grundsätzlich, StateFlow
als Dateninhaber zu behandeln
Assertions für seine value
-Eigenschaft an. Auf diese Weise validieren Tests die aktuellen
Zustand des Objekts zu einem bestimmten Zeitpunkt und hängen nicht davon ab,
kommt es zu einer Zusammenführung.
Nehmen wir als Beispiel dieses ViewModel, das Werte aus einem Repository
und
stellt sie der UI in einem StateFlow
zur Verfügung:
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
}
Beim Testen des ViewModel mit diesem Fake können Sie Werte von der Fake-Daten an
Aktualisierungen im StateFlow
-Element von ViewModel auslösen und dann auf dem aktualisierten
value
:
@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 von stateIn erstellten StateFlows arbeiten
Im vorherigen Abschnitt verwendet ViewModel ein MutableStateFlow
zum Speichern des
Letzter Wert, der von einem Ablauf aus dem Repository
ausgegeben wurde. Dies ist ein gängiges Muster,
normalerweise einfacher mithilfe des
stateIn
, der einen Cold Flow 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 SharingStarted
-Parameter, mit dem festgelegt wird,
wird er aktiv und beginnt,
den zugrunde liegenden Ablauf zu nutzen. Optionen wie
SharingStarted.Lazily
und SharingStarted.WhileSubsribed
werden häufig verwendet
in ViewModels.
Selbst wenn Sie in Ihrem Test auf value
der StateFlow
Anspruch erheben,
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-Koroutinen unter Android testen
- Kotlin-Abläufe unter Android
StateFlow
undSharedFlow
- Zusätzliche Ressourcen für Kotlin-Koroutinen und -Ablauf