Kotlin-Abläufe unter Android testen

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">
</ph>
das Testsubjekt und die Datenschicht
Abbildung 1. Die Testperson und die Daten Ebene.

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">
</ph>
Abhängigkeiten durch eine fiktive Implementierung ersetzt werden.
Abbildung 2. Abhängigkeiten werden durch einen Fake ersetzt Implementierung.

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">
</ph>
Repository mit fiktiven Abhängigkeiten, das einen Ablauf offenlegt
Abbildung 3. Ein Repository (das zu testende Thema) mit Fake-Objekt die einen Datenfluss freigibt.

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