Kotlin-Koroutinen unter Android testen

Code für Einheitentests, der Koroutinen verwendet, erfordert besondere Aufmerksamkeit, da ihre Ausführung asynchron sein kann und über mehrere Threads hinweg erfolgen kann. In diesem Leitfaden wird beschrieben, wie Sperrfunktionen getestet werden können, mit welchen Testkonstrukten Sie vertraut sein müssen und wie Sie Ihren Code erstellen, der Koroutinen testbar verwendet.

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

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

Aussetzende Funktionen in Tests aufrufen

Wenn Sie Anhaltende Funktionen in Tests aufrufen möchten, müssen Sie sich in einer Koroutine befinden. Da die JUnit-Testfunktionen selbst keine Funktionen anhalten, müssen Sie in Ihren Tests einen Coroutine-Builder aufrufen, um eine neue Koroutine zu starten.

runTest ist ein Coroutine-Builder für Tests. Hiermit werden alle Tests zusammengefasst, die Koroutinen enthalten. Koroutinen können nicht nur direkt im Testtext gestartet werden, sondern auch von den im Test verwendeten Objekten.

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

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

Im Allgemeinen sollte pro Test ein Aufruf von runTest verwendet werden. Die Verwendung eines Ausdrucks wird empfohlen.

Wenn Sie den Code Ihres Tests in runTest umschließen, können Sie grundlegende Sperrfunktionen testen. Außerdem werden Verzögerungen in Koroutinen automatisch übersprungen, sodass der obige Test viel schneller als eine Sekunde abgeschlossen wird.

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

  • Wenn Ihr Code neue Koroutinen erstellt, die von der von runTest erstellten Testkoroutine der obersten Ebene stammen, müssen Sie steuern, wie diese neuen Koroutinen geplant werden, indem Sie die entsprechende TestDispatcher auswählen.
  • Wenn Ihr Code die Ausführung der Koroutine zu anderen Disponenten verschiebt (z. B. mithilfe von withContext), funktioniert runTest in der Regel zwar weiterhin, Verzögerungen werden jedoch nicht mehr übersprungen und Tests sind weniger vorhersehbar, da 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. Sie müssen TestDispatchers verwenden, wenn während des Tests neue Koroutinen erstellt werden, damit die Ausführung der neuen Koroutinen vorhersehbar ist.

Es gibt zwei Implementierungen von TestDispatcher: StandardTestDispatcher und UnconfinedTestDispatcher, mit denen 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 zur Freigabe von Planern finden Sie unter TestDispatcher einfügen.

Um die Testkoroutine der obersten Ebene zu starten, erstellt runTest ein TestScope. Das ist eine Implementierung von CoroutineScope, die immer einen TestDispatcher verwendet. Wenn nicht angegeben, erstellt ein TestScope standardmäßig ein StandardTestDispatcher und verwendet dieses, um die Testkoroutine der obersten Ebene auszuführen.

runTest verfolgt die Koroutinen, die sich in der Warteschlange des Planers befinden, der vom Disponenten seiner TestScope verwendet wird, und wird nicht zurückgegeben, solange es ausstehende Aufgaben für diesen Planer gibt.

StandardTestDispatcher

Wenn Sie neue Koroutinen für einen StandardTestDispatcher starten, werden sie im zugrunde liegenden Planer in die Warteschlange gestellt und immer dann ausgeführt, wenn der Testthread verfügbar ist. Damit diese neuen Koroutinen ausgeführt werden können, müssen Sie den Testthread mit yield versehen. So können Sie ihn für andere Koroutinen freigeben. 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 bei der Ausführung der Testkoroutine der obersten Ebene nicht zurückgegeben wird, werden neue Koroutinen erst nach Abschluss der Testkoroutine ausgeführt (aber bevor runTest zurückgegeben wird):

@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 erhalten, damit Koroutinen aus der Warteschlange ausgeführt werden können. Durch alle diese Aufrufe werden andere Koroutinen im Testthread ausgeführt, bevor sie zurückgegeben werden:

  • advanceUntilIdle: Führt alle anderen Koroutinen im Planer aus, bis in der Warteschlange nichts mehr vorhanden ist. Dies ist eine gute Standardeinstellung, um alle ausstehenden Koroutinen laufen zu lassen, und sie funktioniert in den meisten Testszenarien.
  • advanceTimeBy: Verschiebt die virtuelle Zeit um die angegebene Zeit und führt alle Koroutinen aus, die vor diesem Zeitpunkt in der virtuellen Zeit ausgeführt werden sollen.
  • runCurrent: Führt Koroutinen aus, die zur aktuellen virtuellen Zeit geplant sind.

Zum Beheben des vorherigen Tests kann advanceUntilIdle verwendet werden, damit die beiden ausstehenden Koroutinen ihre Arbeit ausführen können, bevor mit der Assertion fortgefahren wird:

@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
}

UnconfinedTestDispatcher

Wenn neue Koroutinen für einen UnconfinedTestDispatcher gestartet werden, werden sie mit Spannung im aktuellen Thread gestartet. Sie werden also sofort gestartet, ohne auf die Rückgabe ihres Koroutinen-Builders warten zu müssen. In vielen Fällen führt dieses Weiterleitungsverhalten zu einfacherem Testcode, da Sie den Testthread nicht manuell zurückgeben müssen, um neue Koroutinen ausführen zu können.

Dieses Verhalten unterscheidet sich jedoch von dem, was Sie in der Produktion bei Nicht-Test-Disponenten beobachten werden. Wenn sich der 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 Standardkoroutine verwenden möchten, erstellen Sie eine Instanz und übergeben Sie sie als Parameter. Dadurch werden neue Koroutinen, die in runTest erstellt wurden, motiviert ausgeführt, da sie den Disponenten 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 Startaufrufe ihre neuen Koroutinen ehrgeizig am UnconfinedTestDispatcher, was bedeutet, dass jeder Startaufruf erst nach Abschluss der Registrierung zurückgegeben wird.

Denk daran, dass UnconfinedTestDispatcher neue Koroutinen freudig startet. Das bedeutet aber nicht, dass sie auch eifrig bis zum Ende ausgeführt werden. Wenn die neue Koroutine aussetzt, wird die Ausführung der anderen Koroutinen fortgesetzt.

Die neue Koroutine, die in diesem Test gestartet wird, registriert beispielsweise Alice, wird aber angehalten, wenn delay aufgerufen wird. Dadurch kann die Koroutine der obersten 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
}

Testdisponenten einschleusen

Zu testender Code kann Disponenten verwenden, um Threads mit withContext zu wechseln oder neue Koroutinen zu starten. Wenn Code in mehreren Threads parallel ausgeführt wird, können die Tests instabil werden. Es kann schwierig sein, Assertions zum richtigen Zeitpunkt 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. Das hat mehrere Vorteile:

  • Der Code wird im einzelnen Test-Thread ausgeführt, was die Tests deterministischer macht
  • Sie können steuern, wie neue Koroutinen geplant und ausgeführt werden
  • TestDispatchers verwenden einen Planer für virtuelle Termine, mit dem Verzögerungen automatisch übersprungen werden und Sie die Zeit manuell verlängern können.

Die Verwendung von Dependency Injection, um Disponenten für Ihre Klassen bereitzustellen, macht es einfach, die echten Disponenten in Tests zu ersetzen. In diesen Beispielen wird ein CoroutineDispatcher eingefügt. Sie können aber auch den breiter gefassten CoroutineContext-Typ injizieren, was für noch mehr Flexibilität während der Tests sorgt.

Bei Klassen, die Koroutinen starten, können Sie auch einen CoroutineScope anstelle eines Disponenten injizieren, wie im Abschnitt Bereich einfügen beschrieben.

TestDispatchers erstellt standardmäßig einen neuen Planer, wenn der Planer instanziiert wird. Innerhalb von runTest können Sie auf die testScheduler-Eigenschaft des TestScope zugreifen und sie an jede neu erstellte TestDispatchers übergeben. Dadurch wird ihr Verständnis von virtueller Zeit vermittelt, und Methoden wie advanceUntilIdle führen Koroutinen für alle Test-Disponenten bis zum Abschluss aus.

Im folgenden Beispiel sehen Sie eine Repository-Klasse, die mithilfe des Disponenten IO in ihrer Methode initialize eine neue Koroutine erstellt und den Aufrufer in ihrer Methode fetchData auf den Disponenten IO 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 einschleusen, um den IO-Disponenten zu ersetzen.

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

fetchData ist auch vom Ausführen in einem TestDispatcher profitieren, da es im Testthread ausgeführt wird und die Verzögerung überspringt, die es während des Tests gibt.

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 gezeigt mit initialize manuell erweitert werden. Beachten Sie jedoch, dass dies in Produktionscode weder möglich noch wünschenswert wäre. Stattdessen sollte diese Methode so umgestaltet werden, dass sie entweder pausiert (für sequenzielle Ausführung) oder einen Deferred-Wert zurückgibt (für gleichzeitige Ausführung).

Sie können beispielsweise async verwenden, um eine neue Koroutine zu starten und eine Deferred zu erstellen:

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

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

Auf diese Weise kannst du die Vervollständigung dieses Codes sowohl in Tests als auch in Produktionscode sicher await:

@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, und gibt sie dann zurück, wenn sich die Koroutinen auf einem TestDispatcher befinden, mit dem sie einen Planer verwendet. Außerdem wird auf Koroutinen gewartet, die der Testkoroutine der obersten Ebene untergeordnet sind, selbst wenn sie sich auf anderen Disponenten befinden (bis zu einem durch den Parameter dispatchTimeoutMs festgelegten Zeitlimit; standardmäßig 60 Sekunden).

Festlegen des Haupt-Disponenten

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 getestete Code auf den Hauptthread verweist, wird bei Einheitentests eine Ausnahme ausgelöst.

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

Hier ein Beispiel für eine ViewModel-Implementierung, die viewModelScope verwendet, 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!"
        }
    }
}

Verwenden Sie die Funktionen Dispatchers.setMain und Dispatchers.resetMain, um den Disponenten Main in allen Fällen durch TestDispatcher zu ersetzen.

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 Disponent Main durch einen TestDispatcher ersetzt wurde, verwenden alle neu erstellten TestDispatchers-Disponenten automatisch den Planer des Disponenten Main, einschließlich des von runTest erstellten Disponenten StandardTestDispatcher, wenn kein anderer Disponent an ihn übergeben wird.

So lässt sich leichter sicherstellen, dass während des Tests nur ein einzelner Planer verwendet wird. Damit dies funktioniert, müssen Sie nach dem Aufruf von Dispatchers.setMain alle anderen TestDispatcher-Instanzen erstellen.

Um eine Duplizierung des Codes zu vermeiden, der den Main-Dispatcher in jedem Test ersetzt, lässt sich ein gängiges Muster in eine JUnit-Testregel 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)
    }
}

Bei dieser Regelimplementierung wird standardmäßig ein UnconfinedTestDispatcher verwendet, aber ein StandardTestDispatcher kann als Parameter übergeben werden, wenn der Main-Dispatcher in einer bestimmten Testklasse nicht motiviert ausführen soll.

Wenn Sie eine TestDispatcher-Instanz im Testtext benötigen, können Sie die testDispatcher aus der Regel wiederverwenden, solange der gewünschte Typ verwendet wird. Wenn du den im Test verwendeten TestDispatcher-Typ explizit angeben möchtest oder ein TestDispatcher benötigst, der nicht mit dem für Main verwendeten Typ übereinstimmt, kannst du in runTest eine neue TestDispatcher erstellen. Da der Main-Disponent auf TestDispatcher gesetzt ist, gibt jeder neu erstellte TestDispatchers-Disponent seinen Planer automatisch 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())
    }
}

Disponenten außerhalb eines Tests erstellen

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

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

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

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

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

Wenn Sie den Disponenten Main wie im vorherigen Abschnitt beschrieben ersetzen, wird der Planer für TestDispatchers, der nach dem Ersetzen des Disponenten Main erstellt wurde, automatisch freigegeben.

Dies gilt jedoch nicht für TestDispatchers, die als Attribute der Testklasse erstellt wurden, oder für TestDispatchers, die während der Initialisierung von Attributen in der Testklasse erstellt wurden. Sie werden initialisiert, bevor der Disponent Main ersetzt wird. Daher werden neue Planer erstellt.

Damit der Test nur einen Planer enthält, müssen Sie zuerst das Attribut MainDispatcherRule erstellen. Verwenden Sie dann nach Bedarf seinen Disponenten (oder seinen Planer, falls Sie einen TestDispatcher eines anderen Typs benötigen) in den Initialisierern anderer Eigenschaften auf Klassenebene.

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

    private val repository = Repository(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...
    }
}

Beachten Sie, dass sowohl runTest als auch TestDispatchers, die im Test erstellt wurden, automatisch den Planer des Disponenten Main freigeben.

Wenn Sie den Disponenten Main nicht ersetzen, erstellen Sie die 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 = Repository(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 vom ersten Dispatcher 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 für diesen Disponenten auszuführen.

Eigenen TestScope erstellen

Wie bei TestDispatchers müssen Sie möglicherweise außerhalb des Testtexts auf ein TestScope zugreifen. runTest erstellt automatisch eine „TestScope“ im Hintergrund. Du kannst aber auch deine eigene TestScope zur Verwendung mit runTest erstellen.

Achten Sie dabei darauf, runTest für die TestScope aufzurufen, die Sie erstellt haben:

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

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

Mit dem Code oben werden implizit ein StandardTestDispatcher und ein neuer Planer für die TestScope erstellt. Diese Objekte können auch alle explizit erstellt werden. Dies kann nützlich sein, wenn Sie Abhängigkeitsinjektionskonfigurationen 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 Tests steuern müssen, können Sie einen Koroutinenbereich in diese Klasse injizieren und in Tests durch einen TestScope ersetzen.

Im folgenden Beispiel hängt die Klasse UserState von einem UserRepository ab, um neue Nutzer zu registrieren und die Liste der registrierten Nutzer abzurufen. Da diese Aufrufe von UserRepository Funktionsaufrufe unterbrechen, verwendet UserState den eingefügten CoroutineScope-Wert, um eine neue Koroutine innerhalb der Funktion registerUser 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() }
        }
    }
}

Um diese Klasse zu testen, können Sie beim Erstellen des UserState-Objekts den TestScope aus runTest übergeben:

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)
    }
}

Informationen zum Einfügen eines Bereichs außerhalb der Testfunktion, z. B. in ein zu testendes Objekt, das als Attribut in der Testklasse erstellt wird, finden Sie unter Eigenen TestScope erstellen.

Weitere Informationen