Android'de Kotlin eş yordamlarını test etme

Eşlikler kullanan birim testi kodu, yürütme eşzamansız olarak gerçekleştirilebileceği ve birden fazla iş parçacığında gerçekleşebileceği için ekstra dikkat gerektirir. Bu kılavuzda, askıya alma işlevlerinin nasıl test edilebileceği, bilmeniz gereken test yapıları ve eş yordamları kullanan kodunuzu nasıl test edilebilir hale getirebileceğiniz ele alınmaktadır.

Bu kılavuzda kullanılan API'ler, kotlinx.coroutines.test kitaplığının bir parçasıdır. Bu API'lere erişmek için yapıyı projenize bir test bağımlılığı olarak eklediğinizden emin olun.

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

Testlerde askıya alma işlevleri çağırma

Testlerde askıya alma işlevlerini çağırmak için bir eş yordamda olmanız gerekir. JUnit test işlevlerinin kendileri işlevleri askıya almadığından, yeni bir eş yordam başlatmak için testlerinizin içinde bir eş yordam oluşturucu çağırmanız gerekir.

runTest, test için tasarlanmış bir eş yordam oluşturucudur. Eş yordam içeren tüm testleri sarmalamak için bunu kullanın. Eş yordamların yalnızca doğrudan test gövdesinde değil, testte kullanılan nesneler tarafından da başlatılabileceğini unutmayın.

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

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

Genel olarak, test başına bir runTest çağrınız olmalıdır ve bir ifade gövdesi kullanmanız önerilir.

Testinizin kodunu runTest içinde sarmalamak, temel askıya alma işlevlerini test etmek için işe yarar ve eş yordamlardaki gecikmeleri otomatik olarak atlayarak yukarıdaki testin bir saniyeden çok daha kısa sürede tamamlanmasını sağlar.

Ancak, test edilen kodunuzda ne olduğuna bağlı olarak dikkat etmeniz gereken bazı ek noktalar vardır:

  • Kodunuz, runTest tarafından oluşturulan üst düzey test eş yordamı dışında yeni eş yordamlar oluşturduğunda, uygun TestDispatcher'yi seçerek bu yeni eş yordamların nasıl planlandığını kontrol etmeniz gerekir.
  • Kodunuz eş yordam yürütmesini başka görev dağıtıcılara taşırsa (örneğin, withContext kullanarak) runTest genel olarak çalışmaya devam eder ancak artık gecikmeler atlanmaz ve kod birden fazla iş parçacığında çalıştırıldığından testler daha az tahmin edilebilir olur. Bu nedenlerden dolayı, testlerde gerçek görev dağıtıcıları değiştirmek için test görev dağıtıcıları eklemeniz gerekir.

Test Sevk görevlileri

TestDispatchers, test amaçlı CoroutineDispatcher uygulamalarıdır. Yeni eş yordamların yürütülmesini tahmin edilebilir hale getirmek için test sırasında yeni eş yordalar oluşturulursa TestDispatchers kullanmanız gerekir.

TestDispatcher için kullanılabilen iki uygulama vardır: StandardTestDispatcher ve UnconfinedTestDispatcher. Bu uygulamalar, yeni başlatılan ortaklar için farklı planlamalar yapar. Her ikisi de sanal zamanı kontrol etmek ve bir test sırasında çalışan eşleri yönetmek için bir TestCoroutineScheduler kullanır.

Bir testte yalnızca bir planlayıcı örneği kullanılmalıdır ve TestDispatchers bunların tümü arasında paylaşılır. Planlayıcıları paylaşma hakkında bilgi edinmek için TestDispatchers Ekleme başlıklı makaleye bakın.

runTest, üst düzey test eş yordamını başlatmak için bir TestScope oluşturur. Bu, her zaman TestDispatcher kullanacak olan bir CoroutineScope uygulamasıdır. Belirtilmezse TestScope varsayılan olarak bir StandardTestDispatcher oluşturur ve üst düzey test eş yordamını çalıştırmak için bunu kullanır.

runTest, TestScope görev dağıtıcısı tarafından kullanılan planlayıcıda sıraya alınan eş değerleri takip eder ve ilgili planlayıcıda beklemede olan çalışmalar olduğu sürece geri gelmez.

StandartTestDispatcher

Bir StandardTestDispatcher üzerinde yeni eş yordamlar başlattığınızda bu eş postalar, test iş parçacığı serbest olduğunda çalıştırılmak üzere temel planlayıcıda sıraya alınır. Bu yeni eş yordamların çalışmasına izin vermek için test iş parçacığını getirmeniz gerekir (diğer eş yapılandırmaları kullanması için serbest bırakın). Bu sıraya alma davranışı, test sırasında yeni eş yordamların nasıl çalışacağı üzerinde hassas bir kontrole sahip olmanızı sağlar ve üretim kodundaki eş yordamların programlanmasına benzer.

Üst düzey test eş yordasının yürütülmesi sırasında test iş parçacığı hiçbir zaman sağlanmazsa yeni eş yordamlar yalnızca test eş yordasının tamamlanmasından sonra (ancak runTest geri dönmeden önce) çalışır:

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

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

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

Sıraya alınan eş yordamların çalışmasını sağlamak için test eş yordamının birkaç yolu vardır. Bu çağrıların tümü, geri dönmeden önce diğer eş yordamlarının test ileti dizisinde çalıştırılmasını sağlar:

  • advanceUntilIdle: Sırada hiçbir şey kalmayana kadar diğer tüm eş yordamları planlayıcıda çalıştırır. Bu, bekleyen tüm eş yordamların çalışmasına izin vermek için iyi bir varsayılan seçenektir ve çoğu test senaryosunda işe yarar.
  • advanceTimeBy: Sanal zamanı belirtilen miktara göre ilerletir ve sanal zamandaki bu noktadan önce çalıştırılmak üzere planlanmış eş yordamları çalıştırır.
  • runCurrent: Geçerli sanal saatte planlanmış eş yordamlar çalıştırır.

Önceki testi düzeltmek amacıyla, onaylamaya devam etmeden önce beklemedeki iki eş yordamın çalışmasını sağlamak için advanceUntilIdle kullanılabilir:

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

Sınırsız Test Görevlisi

Bir UnconfinedTestDispatcher üzerinde yeni eş yordamlar başlatıldığında heyecanla mevcut ileti dizisinde başlatılır. Diğer bir deyişle, eş yordam oluşturucunun geri dönmesini beklemeden hemen çalışmaya başlarlar. Çoğu durumda, bu gönderme davranışı daha basit bir test kodu ile sonuçlanır. Yeni eş yapılandırmaları çalıştırmak için test iş parçacığını manuel olarak oluşturmanız gerekmez.

Ancak bu davranış, test dışı görev dağıtıcılarla üretimde göreceğiniz davranıştan farklıdır. Testiniz eşzamanlılığa odaklanıyorsa bunun yerine StandardTestDispatcher kullanmayı tercih edin.

Bu görev dağıtıcıyı runTest öğesindeki üst düzey test eş yordamı için varsayılan eş yordam yerine kullanmak üzere bir örnek oluşturun ve parametre olarak iletin. Bu işlem, runTest içinde oluşturulan yeni eş yordamlarının, görev dağıtıcıyı TestScope öğesinden devraldığı için istekle yürütülmesini sağlar.

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

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

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

Bu örnekte, lansman çağrıları yeni eş yordamlarını UnconfinedTestDispatcher üzerinde heyecanla başlatacaktır. Diğer bir deyişle, her başlatma çağrısı yalnızca kayıt tamamlandıktan sonra geri dönecektir.

UnconfinedTestDispatcher ürününün yeni eş yordamları hevesle başlatacağını unutmayın. Ancak bu, eş zamanlı programların tamamlanması için onları da istekle çalıştıracağı anlamına gelmez. Yeni eş yordam askıya alınırsa diğer eş yordamlar yürütülmeye devam eder.

Örneğin, bu testte başlatılan yeni eş yordam Ayşe'yi kaydeder, ancak delay çağrıldığında askıya alınır. Bu, üst düzey eş yordamın onaylamaya devam etmesini sağlar ve İbrahim henüz kayıtlı olmadığından test başarısız olur:

@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 görev dağıtıcıları ekleme

Test edilen kod, ileti dizilerini değiştirmek (withContext ile) veya yeni eş postalar başlatmak için görev dağıtıcıları kullanabilir. Kod paralel olarak birden fazla iş parçacığında yürütüldüğünde testler güvenilir olmayabilir. Onayları doğru zamanda gerçekleştirmek veya kontrol sahibi olmadığınız arka plan ileti dizilerinde çalışıyorsa görevlerin tamamlanmasını beklemek zor olabilir.

Testlerde, bu görev dağıtıcıları TestDispatchers örnekleriyle değiştirin. Bunun pek çok avantajı vardır:

  • Kod, tek test iş parçacığında çalışacak ve böylece testleri daha belirleyici hale getirecektir
  • Yeni eş yordamların nasıl planlandığını ve yürütüleceğini kontrol edebilirsiniz
  • TestDispatchers, sanal zaman için gecikmeleri otomatik olarak atlayan ve zamanı manuel olarak ilerletmenizi sağlayan bir planlayıcı kullanır

Sınıflarınıza sevk görevlileri sağlamak için bağımlılık ekleme özelliğini kullanmak, testlerdeki gerçek görev dağıtıcıları değiştirmeyi kolaylaştırır. Bu örneklerde bir CoroutineDispatcher ekleyeceğiz ama testler sırasında daha da fazla esneklik sağlamak için daha geniş olan CoroutineContext türünü de ekleyebilirsiniz.

Eş yordamlar başlatan sınıflar için, Kapsam ekleme bölümünde ayrıntılı olarak açıklandığı gibi, bir görev dağıtıcı yerine CoroutineScope de ekleyebilirsiniz.

TestDispatchers, örneklendiğinde varsayılan olarak yeni bir zamanlayıcı oluşturur. runTest içinde, TestScope öğesinin testScheduler mülküne erişebilir ve bunu yeni oluşturulan TestDispatchers öğesine aktarabilirsiniz. Bu, sanal zamanla ilgili anlayışlarını paylaşır ve advanceUntilIdle gibi yöntemler, testin tamamlanması için tüm test görev dağıtıcılarında eş yordamlar çalıştırır.

Aşağıdaki örnekte, initialize yöntemindeki IO görev dağıtıcısını kullanarak yeni bir eş yordam oluşturan ve fetchData yönteminde çağrıyı IO görev dağıtıcısına geçiren bir Repository sınıfını görebilirsiniz:

// 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"
    }
}

Testlerde, IO görev dağıtıcısının yerine bir TestDispatcher uygulaması ekleyebilirsiniz.

Aşağıdaki örnekte, kod deposuna bir StandardTestDispatcher yerleştirme ve initialize içinde başlatılan yeni eş yordamın devam etmeden önce tamamlandığından emin olmak için advanceUntilIdle kullanıyoruz.

Test ileti dizisinde çalıştırılacağı ve test sırasında içerdiği gecikmeyi atlayacağı için fetchData parametresi de bir TestDispatcher üzerinde çalıştırmaktan faydalanabilir.

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

TestDispatcher üzerinde başlatılan yeni eş yordamlar, yukarıda initialize ile gösterildiği gibi manuel olarak geliştirilebilir. Ancak üretim kodunda bunun mümkün olmadığını ve istenmeyeceğini unutmayın. Bunun yerine bu yöntem, askıya alma (sıralı yürütme için) veya bir Deferred değeri (eşzamanlı yürütme için) döndürecek şekilde yeniden tasarlanmalıdır.

Örneğin, yeni bir eş yordam başlatmak ve bir Deferred oluşturmak için async aracını kullanabilirsiniz:

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

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

Böylece hem testlerde hem de üretim kodunda bu kodun tamamlanmasını güvenli bir şekilde await yapabilirsiniz:

@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())
    // ...
}

Ortaklar, planlayıcıyı paylaştığı bir TestDispatcher üzerindeyse runTest dönmeden önce bekleyen eş yordalarının tamamlanmasını bekler. Ayrıca, diğer görev dağıtıcılarda olsalar bile üst düzey test eş yordasının alt öğeleri olan eş yordamları da bekler (dispatchTimeoutMs parametresi tarafından belirtilen zaman aşımı varsayılan olarak 60 saniyeye kadar).

Ana görev dağıtıcıyı ayarlama

Yerel birim testlerinde, Android kullanıcı arayüzü iş parçacığını saran Main görev dağıtıcısı, bu testler bir Android cihazda değil, yerel bir JVM'de yürütüldüğünden kullanılamaz. Test edilen kodunuz ana iş parçacığına referans veriyorsa birim testleri sırasında bir istisna oluşturur.

Bazı durumlarda, Main görev dağıtıcısını önceki bölümde açıklandığı gibi, diğer görev dağıtıcılarla aynı şekilde yerleştirebilir ve testlerde bir TestDispatcher ile değiştirebilirsiniz. Ancak, viewModelScope gibi bazı API'ler, arka planda sabit kodlu bir Main görev dağıtıcısı kullanır.

Veri yükleyen bir eş yordamı başlatmak için viewModelScope kullanan bir ViewModel uygulaması örneğini burada bulabilirsiniz:

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

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

Main görev dağıtıcısını her durumda TestDispatcher ile değiştirmek için Dispatchers.setMain ve Dispatchers.resetMain işlevlerini kullanın.

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

Main görev dağıtıcısı TestDispatcher ile değiştirildiyse yeni oluşturulan tüm TestDispatchers, otomatik olarak Main görev dağıtıcısındaki planlayıcıyı kullanacaktır. Buna, başka görev dağıtıcı iletilmezse runTest tarafından oluşturulan StandardTestDispatcher de dahildir.

Bu, test sırasında yalnızca tek bir planlayıcının kullanılmasını kolaylaştırır. Bunu gerçekleştirmek için Dispatchers.setMain çağırdıktan sonra diğer tüm TestDispatcher örneklerini oluşturduğunuzdan emin olun.

Her testte Main görev dağıtıcısının yerini alan kodun yinelenmesini önlemek için yaygın olarak kullanılan bir yöntem, kodu bir JUnit test kuralına çıkarmaktır:

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

Bu kural uygulaması, varsayılan olarak bir UnconfinedTestDispatcher kullanır. Ancak Main görev dağıtıcısının belirli bir test sınıfında hızlı bir şekilde yürütülmemesi gerekiyorsa parametre olarak StandardTestDispatcher geçirilebilir.

Test gövdesinde bir TestDispatcher örneğine ihtiyacınız olduğunda, istenen tür olduğu sürece kuraldaki testDispatcher öğesini yeniden kullanabilirsiniz. Testte kullanılan TestDispatcher türünü açıkça belirtmek istiyorsanız veya Main için kullanılandan farklı bir TestDispatcher türüne ihtiyacınız varsa runTest içinde yeni bir TestDispatcher oluşturabilirsiniz. Main görev dağıtıcısı TestDispatcher olarak ayarlandığından, yeni oluşturulan tüm TestDispatchers planlayıcısını otomatik olarak paylaşacaktır.

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

Testin dışında görev dağıtıcıları oluşturma

Bazı durumlarda, test yönteminin dışında bir TestDispatcher sunulması gerekebilir. Örneğin, test sınıfındaki bir özelliğin başlatılması sırasında:

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...
        // ...
    }
}

Önceki bölümde gösterildiği gibi Main görev dağıtıcısını değiştiriyorsanız Main görev dağıtıcısı değiştirildikten sonra oluşturulan TestDispatchers, planlayıcısını otomatik olarak paylaşır.

Ancak bu durum, test sınıfının özellikleri olarak oluşturulan TestDispatchers veya test sınıfındaki özelliklerin başlatılması sırasında oluşturulan TestDispatchers için geçerli değildir. Bunlar, Main görev dağıtıcısı değiştirilmeden önce başlatılır. Bu nedenle, yeni planlayıcılar oluştururlar.

Testinizde yalnızca bir planlayıcı olduğundan emin olmak için önce MainDispatcherRule özelliğini oluşturun. Ardından, görev dağıtıcısını (veya farklı türde bir TestDispatcher öğesine ihtiyacınız varsa planlayıcısını) diğer sınıf düzeyindeki özelliklerin başlatıcılarında gerektiğinde yeniden kullanın.

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

Test sırasında oluşturulan runTest ve TestDispatchers öğelerinin, Main görev dağıtıcısının planlayıcısını otomatik olarak paylaşmaya devam edeceğini unutmayın.

Main görev dağıtıcısını değiştirmiyorsanız sınıfın özelliği olarak ilk TestDispatcher oluşturun (yeni bir planlayıcı oluşturur). Ardından, bu planlayıcıyı hem mülk olarak hem de test içinde her runTest çağrısına ve oluşturulan her yeni TestDispatcher öğesine manuel olarak iletin:

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

Bu örnekte, ilk görev dağıtıcıdaki planlayıcı runTest öğesine iletilir. Bu işlem, söz konusu planlayıcıyı kullanarak TestScope için yeni bir StandardTestDispatcher oluşturur. Ayrıca, sevk görevlisinde test eş yordasını çalıştırmak için görev dağıtıcıyı doğrudan runTest bölümüne geçirebilirsiniz.

Kendi TestScope'unuzu oluşturma

TestDispatchers'da olduğu gibi, test gövdesinin dışında bir TestScope öğesine erişmeniz gerekebilir. runTest, arka planda otomatik olarak bir TestScope oluşturur. Bununla birlikte, runTest ile kullanmak üzere kendi TestScope öğenizi de oluşturabilirsiniz.

Bunu yaparken, oluşturduğunuz TestScope cihazında runTest numarasını aradığınızdan emin olun:

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

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

Yukarıdaki kod, TestScope için dolaylı yoldan bir StandardTestDispatcher ve yeni bir planlayıcı oluşturur. Bu nesnelerin tümü açık bir şekilde de oluşturulabilir. Bu işlem, bağımlı yerleştirme kurulumlarıyla entegre etmeniz gerekiyorsa yararlı olabilir.

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

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

Kapsam ekleme

Testler sırasında kontrol etmeniz gereken eş yordamlar oluşturan bir sınıfınız varsa ilgili sınıfa eş yordam kapsamını ekleyebilir ve testlerde TestScope ile değiştirebilirsiniz.

Aşağıdaki örnekte UserState sınıfı, yeni kullanıcıları kaydetmek ve kayıtlı kullanıcıların listesini getirmek için bir UserRepository kullanıyor. Bu UserRepository çağrıları işlev çağrılarını askıya aldığından, UserState yerleştirilen CoroutineScope öğesini kullanarak registerUser işlevi içinde yeni bir eş yordamı başlatır.

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

Bu sınıfı test etmek için UserState nesnesini oluştururken runTest öğesinden TestScope öğesini geçirebilirsiniz:

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

Test işlevinin dışına, örneğin test sınıfında bir özellik olarak oluşturulan test kapsamındaki bir nesneye bir kapsam eklemek için Kendi TestKapsamınızı oluşturma bölümüne bakın.

Ek kaynaklar