Sanitarna inspekcija


Dakle, restoran je spreman. Imamo kuvare koji delegiraju posao (launch), glavnog šefa koji upravlja životnim vekom kuhinje (CoroutineScope) i švedski sto koji je uvek pun (Flow).

Na papiru, sve izgleda kao remek-delo. Ali evo surove realnosti: asinhroni kod je užasno težak za dokazivanje.

U pravoj kuhinji, ako recept kaže “krčkati 4 sata”, inspektor neće sedeti tamo 240 minuta sa štopericom. Poludeo bi. U svetu programiranja imamo isti problem. Ne možemo da priuštimo da nam CI/CD pipeline stoji u mestu 5 sekundi samo zato što je neki delay(5000) zalutao u kod. Još gori su oni flaky testovi što prođu kod vas na mašini, a padnu na serveru jer je mreža štucnula na milisekundu.

Da bismo prošli inspekciju, a da ne izgubimo razum, moramo da naučimo kako da iskrivimo vreme.

Tajni sastojak: runTest


Staromodno testiranje korutina je uključivalo runBlocking i ručno pozivanje Thread.sleep(). Iskreno? Nemojte to raditi. Sporo je i nepouzdano. Umesto toga, koristimo biblioteku kotlinx-coroutines-test.

Glavna zvezda ovde je runTest. Zamislite ga kao simulaciju kuhinje gde sat kuca samo onako kako vi kažete. Ako vaš kod udari u delay(10_000), runTest zapravo ne čeka. On jednostavno teleportuje virtuelni sat 10 sekundi unapred, trenutno.

1. Simulacija krčkanja (suspend funkcije)

Recimo da preuzimamo podatke o korisniku. Potrebna je sekunda da se simulira mrežni poziv.

Kod:

1
2
3
4
5
6
class UserRepository {
    suspend fun getUser(id: String): String {
        delay(1000) // Ovo bi inače ubilo brzinu vaših testova
        return "Korisnik $id"
    }
}

Test: Koristimo runTest da preskočimo dosadan deo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Test
fun `getUser treba da vrati rezultat trenutno`() = runTest {
    val repository = UserRepository()

    // Pozivamo suspend funkciju
    val user = repository.getUser("123")

    // Test se završava za oko 20ms, iako postoji delay od 1s!
    assertEquals("Korisnik 123", user)
}

2. Praćenje table sa statusom (StateFlow)


Testiranje ViewModel-a je mesto gde se većina ljudi saplete. Želite da vidite kako se UI stanje menja: Idle -> Loading -> Success.

Jedan iskren savet: Ako hardkodujete Dispatchers.IO unutar svojih klasa, bićete u problemu. Uvek ubrizgavajte (inject) dispečere. To je jedini način da ih u testovima zamenite nečim što možete da kontrolišete.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class OrderViewModel(private val testDispatcher: CoroutineDispatcher) {
    private val _uiState = MutableStateFlow("Idle")
    val uiState = _uiState.asStateFlow()

    fun fetchOrder() {
        // Koristimo dispečer koji smo dobili spolja
        CoroutineScope(testDispatcher).launch {
            _uiState.value = "Loading"
            delay(500)
            _uiState.value = "Porudžbina #1 spremna"
        }
    }
}

U testu koristimo advanceUntilIdle(). To je u suštini fast-forward dugme koje izvršava sve što čeka u redu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Test
fun `proveri promene stanja`() = runTest {
    val viewModel = OrderViewModel(StandardTestDispatcher(testScheduler))

    viewModel.fetchOrder()

    // Izvrši sve do prvog delay-a
    advanceUntilIdle() 
    assertEquals("Loading", viewModel.uiState.value)

    // Preskoči 500ms unapred
    advanceTimeBy(500)
    assertEquals("Porudžbina #1 spremna", viewModel.uiState.value)
}

3. Provera švedskog stola (Flow)


Kako testirati strim? Ako naš Flow izbaci tri jela, moramo biti sigurni da stižu po redu i da nijedno nije nestalo.

Najlakši trik? Pretvorite Flow u List. runTest je dovoljno pametan da sačeka da se strim završi (virtuelno) pre nego što uradite asertaciju.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun serveBuffet(): Flow<String> = flow {
    delay(1000); emit("Pasta")
    delay(1000); emit("Salata")
    delay(1000); emit("Hleb")
}

@Test
fun `proveri redosled jela`() = runTest {
    // toList() skuplja sve emisije u jedan paket
    val results = serveBuffet().toList() 
        
    assertEquals(listOf("Pasta", "Salad", "Bread"), results)
}

Finale serijala: Kuhinja je otvorena


Testiranje nije birokratija. To je način da ne dobijete poziv u 3 ujutru jer se aplikacija srušila. Koristeći runTest, prelazimo sa “valjda ovo radi” na “imam dokaz da radi.”

U ovom serijalu smo prešli ozbiljan put:

  1. Osnove: Prestali smo da blokiramo tredove.
  2. Menadžment: Koristili smo Scope-ove da sprečimo curenje memorije.
  3. Strimovi: Ovladali smo Flow-om za kompleksne podatke.
  4. Otpornost: Preživeli smo gužvu uz backpressure.
  5. Verifikacija: Naučili smo da krivimo vreme radi testova.

Kuhinja je sada vaša. Napravite nešto brzo i stabilno.

Srećno kodiranje!