Kotlin-Koroutinen unter Android testen

Einheitentestcode, der Coroutinen verwendet, erfordert besondere Aufmerksamkeit, da ihre Ausführung möglicherweise asynchron sein kann und über mehrere Threads hinweg ausgeführt wird. In diesem Leitfaden wird beschrieben, wie Aussetzen von Funktionen getestet werden kann, welche Testkonstrukte Sie kennen sollten und wie Sie Ihren Code, der Koroutinen verwendet, testbar machen.

Die in diesem Leitfaden verwendeten APIs sind Teil der Bibliothek kotlinx.coroutines.test. Fügen Sie das Artefakt Ihrem Projekt als Testabhängigkeit hinzu, um Zugriff auf diese APIs zu haben.

dependencies {
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}

Aussetzende Funktionen in Tests aufrufen

Um Aussetzungsfunktionen in Tests aufzurufen, müssen Sie sich in einer Koroutine befinden. Da JUnit-Testfunktionen selbst keine Funktionen aussetzen, müssen Sie in Ihren Tests einen Coroutine-Builder aufrufen, um eine neue Koroutine zu starten.

runTest ist ein Tool zur Erstellung von Koroutinen, das zum Testen entwickelt wurde. Verwenden Sie dies, um alle Tests zu umschließen, die Koroutinen enthalten. Koroutinen können nicht nur direkt im Testkörper gestartet werden, sondern auch durch die im Test verwendeten Objekte.

suspend fun fetchData(): String {
    delay(1000L)
    return "Hello world"
}

@Test
fun dataShouldBeHelloWorld() = runTest {
    val data = fetchData()
    assertEquals("Hello world", data)
}

Im Allgemeinen sollten Sie einen Aufruf von runTest pro Test haben. Die Verwendung eines Ausdrucks wird empfohlen.

Wenn Sie den Code Ihres Tests in runTest umschließen, können Sie grundlegende Sperrenfunktionen testen. Verzögerungen bei Koroutinen werden automatisch übersprungen und der obige Test ist viel schneller als eine Sekunde.

Je nachdem, was in dem zu testenden Code geschieht, sind jedoch weitere Überlegungen erforderlich:

  • Wenn mit Ihrem Code neue Koroutinen erstellt werden, die nicht von der von runTest erstellten Testkoroutine der obersten Ebene stammen, müssen Sie steuern, wie diese neuen Koroutinen geplant werden. Wählen Sie dazu die entsprechende TestDispatcher aus.
  • Wenn Ihr Code die Ausführung der gemeinsamen Routine an andere Disponenten verschiebt (z. B. mit withContext), funktioniert runTest in der Regel weiterhin. Verzögerungen werden jedoch nicht mehr übersprungen und Tests sind weniger vorhersehbar, wenn der Code in mehreren Threads ausgeführt wird. Aus diesen Gründen sollten Sie in Tests Test-Disponenten einfügen, um echte Disponenten zu ersetzen.

TestDispatcher

TestDispatchers sind CoroutineDispatcher-Implementierungen zu Testzwecken. Wenn während des Tests neue Koroutinen erstellt werden, müssen Sie TestDispatchers verwenden, damit die Ausführung der neuen Koroutinen vorhersehbar ist.

Es gibt zwei verfügbare Implementierungen von TestDispatcher: StandardTestDispatcher und UnconfinedTestDispatcher. Sie führen dazu, dass neu gestartete Koroutinen unterschiedlich geplant werden. Beide verwenden ein TestCoroutineScheduler, um die virtuelle Zeit zu steuern und laufende Koroutinen innerhalb eines Tests zu verwalten.

In einem Test sollte nur eine Planerinstanz verwendet werden, die von allen TestDispatchers gemeinsam genutzt wird. Weitere Informationen zu Freigabeplanern finden Sie unter TestDispatchers einfügen.

Zum Starten der Testkoroutine auf oberster Ebene erstellt runTest eine TestScope. Dabei handelt es sich um eine Implementierung von CoroutineScope, die immer einen TestDispatcher verwendet. Wenn nicht angegeben, erstellt ein TestScope standardmäßig eine StandardTestDispatcher und verwendet diese, um die Testkoroutine der obersten Ebene auszuführen.

runTest verfolgt die Koroutinen, die sich in der Warteschlange des Planers befinden, der vom Disponenten seines TestScope verwendet wird, und wird nicht zurückgegeben, solange an diesem Planer Arbeiten ausstehen.

StandardTestDispatcher

Wenn Sie neue Koroutinen auf einem StandardTestDispatcher starten, werden sie im zugrunde liegenden Planer in die Warteschlange gestellt und immer dann ausgeführt, wenn der Testthread kostenlos verwendet werden kann. Damit diese neuen Koroutinen ausgeführt werden können, müssen Sie den Testthread freigeben. Er muss für andere Koroutinen freigegeben werden. Mit diesem Warteschlangenverhalten können Sie genau steuern, wie neue Koroutinen während des Tests ausgeführt werden. Es ähnelt der Planung von Koroutinen im Produktionscode.

Wenn der Testthread während der Ausführung der Testkoroutine der obersten Ebene nie zurückgegeben wird, werden alle neuen Koroutinen erst ausgeführt, nachdem die Testkoroutine abgeschlossen ist (aber bevor runTest Folgendes zurückgibt):

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
}

Es gibt mehrere Möglichkeiten, die Testkoroutine zu erzeugen, damit Koroutinen in der Warteschlange ausgeführt werden können. Bei allen diesen Aufrufen werden andere Koroutinen im Testthread ausgeführt, bevor sie zurückgegeben werden:

  • advanceUntilIdle: Führt alle anderen Koroutinen im Planer aus, bis keine Einträge mehr in der Warteschlange sind. Dies ist eine gute Standardauswahl, damit alle ausstehenden Koroutinen ausgeführt werden können. Sie funktioniert in den meisten Testszenarien.
  • advanceTimeBy: Erhöht die virtuelle Zeit um den angegebenen Betrag und führt alle Koroutinen aus, die vor diesem Zeitpunkt in virtueller Zeit ausgeführt werden sollen.
  • runCurrent: Führt Koroutinen aus, die zur aktuellen virtuellen Zeit geplant sind.

Um den vorherigen Test zu korrigieren, kann advanceUntilIdle verwendet werden, damit die beiden ausstehenden Koroutinen ihre Arbeit ausführen, bevor sie mit der Assertion fortfahren:

@Test
fun standardTest() = runTest {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }
    advanceUntilIdle() // Yields to perform the registrations

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

UnlimitedTestDispatcher

Wenn neue Koroutinen auf einem UnconfinedTestDispatcher gestartet werden, werden sie eifrig im aktuellen Thread gestartet. Das bedeutet, dass sie sofort ausgeführt werden und nicht auf die Rückgabe des Coroutine-Builders warten müssen. In vielen Fällen führt dieses Weiterleitungsverhalten zu einem einfacheren Testcode, da Sie den Testthread nicht manuell bereitstellen müssen, damit neue Koroutinen ausgeführt werden können.

Dieses Verhalten unterscheidet sich jedoch von dem, was Sie in der Produktion bei Nicht-Test-Disponenten sehen. Wenn sich Ihr Test auf Nebenläufigkeit konzentriert, sollten Sie stattdessen StandardTestDispatcher verwenden.

Wenn Sie diesen Dispatcher für die Testkoroutine der obersten Ebene in runTest anstelle der Standardroutine verwenden möchten, erstellen Sie eine Instanz und übergeben Sie sie als Parameter. Dadurch werden neue Koroutinen, die in runTest erstellt wurden, eifrig ausgeführt, da sie den Dispatcher von TestScope übernehmen.

@Test
fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch { userRepo.register("Alice") }
    launch { userRepo.register("Bob") }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}

In diesem Beispiel starten die Launch-Aufrufe ihre neuen Koroutinen eifrig auf der UnconfinedTestDispatcher. Das bedeutet, dass jeder Startaufruf erst nach Abschluss der Registrierung zurückgegeben wird.

Denk daran, dass UnconfinedTestDispatcher eifrig neue gemeinsame Routinen startet. Das bedeutet aber nicht, dass sie auch ohne Unterbrechung bis zum Ende ausgeführt werden. Wenn die neue Koroutine anhält, werden andere Koroutinen weiter ausgeführt.

Durch die neue Koroutine, die in diesem Test gestartet wird, wird beispielsweise Alice registriert. Sie wird aber beim Aufrufen von delay angehalten. Dadurch kann die Koroutine auf oberster Ebene mit der Assertion fortfahren und der Test schlägt fehl, da Bob noch nicht registriert ist:

@Test
fun yieldingTest() = runTest(UnconfinedTestDispatcher()) {
    val userRepo = UserRepository()

    launch {
        userRepo.register("Alice")
        delay(10L)
        userRepo.register("Bob")
    }

    assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
}

Test-Dispatcher einfügen

Für zu testenden Code können Disponenten verwendet werden, um Threads zu wechseln (mit withContext) oder neue Koroutinen zu starten. Wenn Code parallel auf mehreren Threads ausgeführt wird, können die Tests instabil werden. Es kann schwierig sein, Assertions zur richtigen Zeit auszuführen oder auf den Abschluss von Aufgaben zu warten, wenn sie in Hintergrundthreads ausgeführt werden, über die Sie keine Kontrolle haben.

Ersetzen Sie diese Disponenten in Tests durch Instanzen von TestDispatchers. Dies hat mehrere Vorteile:

  • Der Code wird für den einzelnen Testthread ausgeführt, wodurch die Tests deterministischer sind
  • Sie können festlegen, wie neue Koroutinen geplant und ausgeführt werden
  • TestDispatcher verwenden einen Planer für die virtuelle Zeit, bei dem Verzögerungen automatisch übersprungen und die Zeit manuell verschoben werden kann.

Mit Abhängigkeitseinschleusung können Sie können Sie die echten Disponenten in Ihren Kursen Tests durchführen. In diesen Beispielen fügen wir ein CoroutineDispatcher ein. Sie können aber auch die breitere Stakeholder CoroutineContext sodass Sie bei Tests noch mehr Flexibilität haben.

Für Klassen, die Koroutinen starten, können Sie auch ein CoroutineScope einfügen. anstatt eines Disponenten, wie unter Bereich einschleusen beschrieben. .

TestDispatchers erstellt standardmäßig bei der Instanziierung einen neuen Planer. In runTest kannst du auf die testScheduler-Property von TestScope zugreifen und sie an neu erstellte TestDispatchers übergeben. Dadurch teilen sie ihr Wissen zur virtuellen Zeit und Methoden wie advanceUntilIdle führen bis zum Abschluss auf allen Test-Dispatchern Koroutinen aus.

Im folgenden Beispiel sehen Sie eine Repository-Klasse, die eine neue Koroutine erstellt, die den IO-Dispatcher in ihrer initialize-Methode verwendet und den Aufrufer zum IO-Dispatcher in der fetchData-Methode umstellt:

// Example class demonstrating dispatcher use cases
class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)
    val initialized = AtomicBoolean(false)

    // A function that starts a new coroutine on the IO dispatcher
    fun initialize() {
        scope.launch {
            initialized.set(true)
        }
    }

    // A suspending function that switches to the IO dispatcher
    suspend fun fetchData(): String = withContext(ioDispatcher) {
        require(initialized.get()) { "Repository should be initialized first" }
        delay(500L)
        "Hello world"
    }
}

In Tests können Sie eine TestDispatcher-Implementierung einfügen, um den IO-Dispatcher zu ersetzen.

Im folgenden Beispiel fügen wir eine StandardTestDispatcher in das Repository ein und verwenden advanceUntilIdle, um sicherzustellen, dass die in initialize gestartete Koroutine abgeschlossen ist, bevor Sie fortfahren.

fetchData profitiert auch von der Ausführung auf einem TestDispatcher, da es im Testthread ausgeführt wird und die Verzögerung während des Tests überspringt.

class RepositoryTest {
    @Test
    fun repoInitWorksAndDataIsHelloWorld() = runTest {
        val dispatcher = StandardTestDispatcher(testScheduler)
        val repository = Repository(dispatcher)

        repository.initialize()
        advanceUntilIdle() // Runs the new coroutine
        assertEquals(true, repository.initialized.get())

        val data = repository.fetchData() // No thread switch, delay is skipped
        assertEquals("Hello world", data)
    }
}

Neue Koroutinen, die auf einem TestDispatcher gestartet wurden, können wie oben mit initialize gezeigt manuell erweitert werden. Beachten Sie jedoch, dass dies im Produktionscode weder möglich noch wünschenswert ist. Stattdessen sollte diese Methode so umgestaltet werden, dass sie entweder angehalten wird (zur sequenziellen Ausführung) oder einen Deferred-Wert zurückgibt (bei gleichzeitiger Ausführung).

Sie können beispielsweise mit async eine neue Koroutine starten und eine Deferred erstellen:

class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
    private val scope = CoroutineScope(ioDispatcher)

    fun initialize() = scope.async {
        // ...
    }
}

Auf diese Weise können Sie die Ausführung dieses Codes sowohl in Tests als auch im Produktionscode sicher mit await ausführen:

@Test
fun repoInitWorks() = runTest {
    val dispatcher = StandardTestDispatcher(testScheduler)
    val repository = BetterRepository(dispatcher)

    repository.initialize().await() // Suspends until the new coroutine is done
    assertEquals(true, repository.initialized.get())
    // ...
}

runTest wartet, bis ausstehende Koroutinen abgeschlossen sind, bevor sie zurückkehren, wenn sich diese Koroutinen auf einer TestDispatcher befinden, mit der sie einen Planer teilt. Außerdem wartet sie auf Koroutinen, die der Testkoroutine der obersten Ebene untergeordnet sind, auch wenn sie sich auf anderen Disponenten befinden (bis zu einem durch den Parameter dispatchTimeoutMs festgelegten Zeitlimit, das standardmäßig 60 Sekunden beträgt).

Festlegen des Haupt-Dispatcher

Bei lokalen Einheitentests ist der Main-Dispatcher, der den Android-UI-Thread umschließt, nicht verfügbar, da diese Tests auf einer lokalen JVM und nicht auf einem Android-Gerät ausgeführt werden. Wenn der zu testende Code auf den Hauptthread verweist, wird während der Einheitentests eine Ausnahme ausgelöst.

In einigen Fällen können Sie den Main-Disponenten auf dieselbe Weise wie andere Disponenten injizieren, wie im vorherigen Abschnitt beschrieben, sodass Sie ihn in Tests durch TestDispatcher ersetzen können. Einige APIs wie viewModelScope verwenden jedoch im Hintergrund einen hartcodierten Main-Dispatcher.

Hier ein Beispiel für eine ViewModel-Implementierung, bei der viewModelScope verwendet wird, um eine Koroutine zu starten, die Daten lädt:

class HomeViewModel : ViewModel() {
    private val _message = MutableStateFlow("")
    val message: StateFlow<String> get() = _message

    fun loadMessage() {
        viewModelScope.launch {
            _message.value = "Greetings!"
        }
    }
}

Wenn Sie den Main-Dispatcher in allen Fällen durch einen TestDispatcher ersetzen möchten, verwenden Sie die Funktionen Dispatchers.setMain und Dispatchers.resetMain.

class HomeViewModelTest {
    @Test
    fun settingMainDispatcher() = runTest {
        val testDispatcher = UnconfinedTestDispatcher(testScheduler)
        Dispatchers.setMain(testDispatcher)

        try {
            val viewModel = HomeViewModel()
            viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly
            assertEquals("Greetings!", viewModel.message.value)
        } finally {
            Dispatchers.resetMain()
        }
    }
}

Wenn der Main-Dispatcher durch einen TestDispatcher ersetzt wurde, verwendet jeder neu erstellte TestDispatchers automatisch den Planer des Main-Disponenten, einschließlich des StandardTestDispatcher, der von runTest erstellt wurde, falls kein anderer Dispatcher an ihn übergeben wird.

So lässt sich leichter sicherstellen, dass während des Tests nur ein einziger Planer verwendet wird. Damit dies funktioniert, müssen Sie alle anderen TestDispatcher-Instanzen erstellen, nachdem Dispatchers.setMain aufgerufen wurde.

Um das Duplizieren des Codes, der den Main-Dispatcher in jedem Test ersetzt, zu vermeiden, besteht die Möglichkeit, ihn in eine JUnit-Testregel zu extrahieren:

// Reusable JUnit4 TestRule to override the Main dispatcher
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

class HomeViewModelTestUsingRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun settingMainDispatcher() = runTest { // Uses Main’s scheduler
        val viewModel = HomeViewModel()
        viewModel.loadMessage()
        assertEquals("Greetings!", viewModel.message.value)
    }
}

Diese Regelimplementierung verwendet standardmäßig eine UnconfinedTestDispatcher, aber ein StandardTestDispatcher kann als Parameter übergeben werden, wenn der Main-Dispatcher in einer bestimmten Testklasse nicht eifrig ausgeführt werden soll.

Wenn Sie eine TestDispatcher-Instanz im Testtext benötigen, können Sie die testDispatcher aus der Regel wiederverwenden, solange es sich um den gewünschten Typ handelt. Wenn Sie explizit den Typ von TestDispatcher angeben möchten, der im Test verwendet wird, oder wenn Sie einen TestDispatcher-Typ benötigen, der von dem für Main verwendeten Typ abweicht, können Sie eine neue TestDispatcher innerhalb von runTest erstellen. Da der Main-Dispatcher auf TestDispatcher gesetzt ist, gibt jede neu erstellte TestDispatchers automatisch seinen Planer frei.

class DispatcherTypesTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler
        // Use the UnconfinedTestDispatcher from the Main dispatcher
        val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher)

        // Create a new StandardTestDispatcher (uses Main’s scheduler)
        val standardRepo = Repository(StandardTestDispatcher())
    }
}

Erstellen von Disponenten außerhalb eines Tests

In einigen Fällen muss möglicherweise ein TestDispatcher außerhalb der Testmethode verfügbar sein. Zum Beispiel während der Initialisierung einer Eigenschaft in der Testklasse:

class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ }

class RepositoryTestWithRule {
    private val repository = ExampleRepository(/* What TestDispatcher? */)

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun someRepositoryTest() = runTest {
        // Test the repository...
        // ...
    }
}

Wenn Sie den Main-Disponenten ersetzen, wie im vorherigen Abschnitt gezeigt, gibt TestDispatchers, die nach der Ersetzung des Main-Disponenten erstellt wurde, automatisch seinen Planer frei.

Bei TestDispatchers, das als Attribute der Testklasse erstellt wird, oder bei TestDispatchers, das während der Initialisierung der Attribute in der Testklasse erstellt wurde, ist dies jedoch nicht der Fall. Diese werden initialisiert, bevor der Main-Dispatcher ersetzt wird. Daher werden neue Planer erstellt.

Damit es in Ihrem Test nur einen Planer gibt, müssen Sie zuerst die Property MainDispatcherRule erstellen. Verwenden Sie dann nach Bedarf seinen Dispatcher (oder seinen Planer, falls Sie ein TestDispatcher eines anderen Typs benötigen) in den Initialisierener anderer Attribute auf Klassenebene.

class RepositoryTestWithRule {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val repository = ExampleRepository(mainDispatcherRule.testDispatcher)

    @Test
    fun someRepositoryTest() = runTest { // Takes scheduler from Main
        // Any TestDispatcher created here also takes the scheduler from Main
        val newTestDispatcher = StandardTestDispatcher()

        // Test the repository...
    }
}

Sowohl für runTest als auch für TestDispatchers, die im Test erstellt wurden, wird weiterhin automatisch der Planer des Main-Disponenten freigegeben.

Wenn Sie den Main-Dispatcher nicht ersetzen, erstellen Sie Ihre erste TestDispatcher (wodurch ein neuer Planer erstellt wird) als Eigenschaft der Klasse. Übergeben Sie diesen Planer dann manuell an jeden runTest-Aufruf und jeden neu erstellten TestDispatcher, sowohl als Attribute als auch innerhalb des Tests:

class RepositoryTest {
    // Creates the single test scheduler
    private val testDispatcher = UnconfinedTestDispatcher()
    private val repository = ExampleRepository(testDispatcher)

    @Test
    fun someRepositoryTest() = runTest(testDispatcher.scheduler) {
        // Take the scheduler from the TestScope
        val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler)
        // Or take the scheduler from the first dispatcher, they’re the same
        val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler)

        // Test the repository...
    }
}

In diesem Beispiel wird der Planer des ersten Disponenten an runTest übergeben. Dadurch wird mit diesem Planer eine neue StandardTestDispatcher für die TestScope erstellt. Sie können den Disponenten auch direkt an runTest übergeben, um die Testkoroutine auf diesem Disponenten auszuführen.

Eigenes TestScope erstellen

Wie bei TestDispatchers musst du möglicherweise außerhalb des Testkörpers auf ein TestScope zugreifen. Mit runTest wird automatisch eine TestScope im Hintergrund erstellt. Du kannst aber auch deine eigene TestScope erstellen und mit runTest verwenden.

Dabei sollten Sie unbedingt runTest auf der von Ihnen erstellten TestScope aufrufen:

class SimpleExampleTest {
    val testScope = TestScope() // Creates a StandardTestDispatcher

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

Mit dem obigen Code werden implizit ein StandardTestDispatcher für die TestScope sowie ein neuer Planer erstellt. Diese Objekte können auch alle explizit erstellt werden. Dies kann nützlich sein, wenn Sie sie in Konfigurationen für Abhängigkeitsinjektionen einbinden müssen.

class ExampleTest {
    val testScheduler = TestCoroutineScheduler()
    val testDispatcher = StandardTestDispatcher(testScheduler)
    val testScope = TestScope(testDispatcher)

    @Test
    fun someTest() = testScope.runTest {
        // ...
    }
}

Bereich einfügen

Wenn Sie eine Klasse haben, die Koroutinen erstellt, die Sie während der können Sie einen Koroutinebereich in diese Klasse einfügen und ihn durch einen TestScope in Tests.

Im folgenden Beispiel hängt die Klasse UserState von einem UserRepository-Element ab um neue Nutzer zu registrieren und die Liste der registrierten Nutzer abzurufen. Da diese Anrufe bis UserRepository Funktionsaufrufe anhalten, UserState verwendet die injizierten CoroutineScope, um eine neue Koroutine in ihrer registerUser-Funktion zu starten.

class UserState(
    private val userRepository: UserRepository,
    private val scope: CoroutineScope,
) {
    private val _users = MutableStateFlow(emptyList<String>())
    val users: StateFlow<List<String>> = _users.asStateFlow()

    fun registerUser(name: String) {
        scope.launch {
            userRepository.register(name)
            _users.update { userRepository.getAllUsers() }
        }
    }
}

Zum Testen dieser Klasse können Sie beim Erstellen der Klasse TestScope aus runTest übergeben. Das Objekt UserState:

class UserStateTest {
    @Test
    fun addUserTest() = runTest { // this: TestScope
        val repository = FakeUserRepository()
        val userState = UserState(repository, scope = this)

        userState.registerUser("Mona")
        advanceUntilIdle() // Let the coroutine complete and changes propagate

        assertEquals(listOf("Mona"), userState.users.value)
    }
}

Um einen Bereich außerhalb der Testfunktion einzufügen, z. B. in ein Objekt unter Test, der als Eigenschaft in der Testklasse erstellt wird, finden Sie unter Eigenen TestScope erstellen

Weitere Informationen