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 entsprechendeTestDispatcher
aus. - Wenn Ihr Code die Ausführung der gemeinsamen Routine an andere Disponenten verschiebt (z. B. mit
withContext
), funktioniertrunTest
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