=================
== The Archive ==
=================

[Kotlin Coroutines] 25장. 플로우 테스트하기

|

Note

변환 함수

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class ObserveAppointmentsService(
    private val appointmentRepository: AppointmentRepository,
) {
    fun observeAppointments(): Flow<List<Appointment>> =
        appointmentRepository
            .observeAppointments()
            .filterIsInstance<AppointmentsUpdate>()
            .map { it.appointments }
            .distinctUntilChanged()
            .retry {
                it is ApiException && it.code in 500..599
            }
}

갱신된 약속만 유지

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
fun `should keep only appointments from updates`() =
    runTest {
        // given
        val repo =
            FakeAppointmentRepository(
                flowOf(
                    AppointmentsConfirmed,
                    AppointmentsUpdate(listOf(anAppointment1)),
                    AppointmentsUpdate(listOf(anAppointment2)),
                    AppointmentsConfirmed,
                ),
            )
        val service = ObserveAppointmentsService(repo)

        // when
        val result = service.observeAppointments().toList()

        // then
        assertEquals(
            listOf(
                listOf(anAppointment1),
                listOf(anAppointment2),
            ),
            result,
        )
    }

이전 원소와 동일한 원소는 제거

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `should eliminate elements that same as the previous one`() =
    runTest {
        // given
        val repo =
            FakeAppointmentRepository(
                flow {
                    delay(1000)
                    emit(AppointmentsUpdate(listOf(anAppointment1)))
                    emit(AppointmentsUpdate(listOf(anAppointment1)))
                    delay(1000)
                    emit(AppointmentsUpdate(listOf(anAppointment2)))
                    delay(1000)
                    emit(AppointmentsUpdate(listOf(anAppointment2)))
                    emit(AppointmentsUpdate(listOf(anAppointment1)))
                },
            )
        val service = ObserveAppointmentsService(repo)

        // when
        val result =
            service.observeAppointments()
                .map { currentTime to it }
                .toList()

        // then
        assertEquals(
            listOf(
                1000L to listOf(anAppointment1),
                2000L to listOf(anAppointment2),
                3000L to listOf(anAppointment1),
            ),
            result,
        )
    }

5XX 에러 코드를 가진 API 예외가 발생한다면 재시도해야 함

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Test
fun `should retry when API exception`() =
    runTest {
        // given
        val repo =
            FakeAppointmentRepository(
                flow {
                    emit(AppointmentsUpdate(listOf(anAppointment1)))
                    throw ApiException(502, "Some message")
                },
            )
        val service = ObserveAppointmentsService(repo)

        // when
        val result =
            service.observeAppointments()
                .take(3)
                .toList()

        // then
        assertEquals(
            listOf(
                listOf(anAppointment1),
                listOf(anAppointment1),
                listOf(anAppointment1),
            ),
            result,
        )
    }

@Test
fun `should retry when API exception with the code 5XX`() =
    runTest {
        // given
        var retried = false
        val someException = object : Exception() {}
        val repo =
            FakeAppointmentRepository(
                flow {
                    emit(AppointmentsUpdate(listOf(anAppointment1)))
                    if (!retried) {
                        retried = true
                        throw ApiException(502, "Some message")
                    } else {
                        throw someException
                    }
                },
            )
        val service = ObserveAppointmentsService(repo)

        // when
        val result =
            service.observeAppointments()
                .catch<Any> { emit(it) }
                .toList()

        // then
        assertTrue(retried)
        assertEquals(
            listOf(
                listOf(anAppointment1),
                listOf(anAppointment1),
                someException,
            ),
            result,
        )
    }

끝나지 않는 플로우 테스트하기

특정 사용자로부터 온 메시지를 감지하는 서비스

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class MessagesService(
    messagesSource: Flow<Message>,
    scope: CoroutineScope,
) {
    private val source = messagesSource
        .shareIn(
            scope = scope,
            started = SharingStarted.WhileSubscribed()
        )

    fun observeMessages(fromUserId: String) = source
        .filter { it.fromUserId == fromUserId }
}

이해하기 쉽게 실패하는 테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Disabled("Failing test!")
@Test
fun `should emit messages from user`() =
    runTest {
        // given
        val source =
            flowOf(
                Message(fromUserId = "0", text = "A"),
                Message(fromUserId = "1", text = "B"),
                Message(fromUserId = "0", text = "C"),
            )
        val service =
            MessagesService(
                messagesSource = source,
                scope = backgroundScope,
            )

        // when
        val result =
            service.observeMessages("0")
                .toList() // Here we'll wait forever!

        // then
        assertEquals(
            listOf(
                Message(fromUserId = "0", text = "A"),
                Message(fromUserId = "0", text = "C"),
            ),
            result,
        )
    }

take 를 사용하는 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
fun `should emit messages from user using take`() =
    runTest {
        // given
        val source =
            flowOf(
                Message(fromUserId = "0", text = "A"),
                Message(fromUserId = "1", text = "B"),
                Message(fromUserId = "0", text = "C"),
            )
        val service =
            MessagesService(
                messagesSource = source,
                scope = backgroundScope,
            )

        // when
        val result =
            service.observeMessages("0")
                .take(2)
                .toList()

        // then
        assertEquals(
            listOf(
                Message(fromUserId = "0", text = "A"),
                Message(fromUserId = "0", text = "C"),
            ),
            result,
        )
  }

backgroundScope 를 사용하는 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Test
fun `should emit messages from user using backgroundScope`() =
    runTest {
        // given
        val source =
            flow {
                emit(Message(fromUserId = "0", text = "A"))
                delay(1000)
                emit(Message(fromUserId = "1", text = "B"))
                emit(Message(fromUserId = "0", text = "C"))
            }
        val service =
            MessagesService(
                messagesSource = source,
                scope = backgroundScope,
            )

        // when
        val emittedMessages = mutableListOf<Message>()
        service.observeMessages("0")
            .onEach { emittedMessages.add(it) }
            .launchIn(backgroundScope)
        delay(1)

        // then
        assertEquals(
            listOf(
                Message(fromUserId = "0", text = "A"),
            ),
            emittedMessages,
        )

        // when
        delay(1000)

        // then
        assertEquals(
            listOf(
                Message(fromUserId = "0", text = "A"),
                Message(fromUserId = "0", text = "C"),
            ),
            emittedMessages,
        )
    }

짧은 시간 동안만 감지할 수 있는 toList 함수를 사용하는 방법

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private suspend fun <T> Flow<T>.toListDuring(duration: Duration): List<T> =
    coroutineScope {
        val result = mutableListOf<T>()
        val job =
            launch {
                this@toListDuring.collect(result::add)
            }
        delay(duration)
        job.cancel()
        return@coroutineScope result
    }

@Test
fun `should emit messages from user using toListDuring`() =
    runTest {
        // given
        val source =
            flow {
                emit(Message(fromUserId = "0", text = "A"))
                emit(Message(fromUserId = "1", text = "B"))
                emit(Message(fromUserId = "0", text = "C"))
            }
        val service =
            MessagesService(
                messagesSource = source,
                scope = backgroundScope,
            )

        // when
        val emittedMessages =
            service.observeMessages("0")
                .toListDuring(1.milliseconds)

        // then
        assertEquals(
            listOf(
                Message(fromUserId = "0", text = "A"),
                Message(fromUserId = "0", text = "C"),
            ),
            emittedMessages,
        )
    }

Turbine 과 같은 라이브러리를 사용하는 방법

1
2
3
4
5
6
7
repositories {
		mavenCentral()
}

dependencies {
		testImplementation("app.cash.turbine:turbine:1.1.0")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Test
fun `should emit messages from user using turbine`() =
    runTest {
        turbineScope {
            // given
            val source =
                flow {
                    emit(Message(fromUserId = "0", text = "A"))
                    emit(Message(fromUserId = "1", text = "B"))
                    emit(Message(fromUserId = "0", text = "C"))
                }
            val service =
                MessagesService(
                    messagesSource = source,
                    scope = backgroundScope,
                )

            // when
            val messagesTurbine =
                service
                    .observeMessages("0")
                    .testIn(backgroundScope)

            // then
            assertEquals(
                Message(fromUserId = "0", text = "A"),
                messagesTurbine.awaitItem(),
            )
            assertEquals(
                Message(fromUserId = "0", text = "C"),
                messagesTurbine.awaitItem(),
            )
            messagesTurbine.expectNoEvents()
        }
    }

개방할 연결 개수 정하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MessagesService(
    private val messagesSource: Flow<Message>,
    scope: CoroutineScope,
) {
    private val source =
        messagesSource
            .shareIn(
                scope = scope,
                started = SharingStarted.WhileSubscribed(),
            )

    fun observeMessages(fromUserId: String) =
        source
            .filter { it.fromUserId == fromUserId }

    fun observeMessagesUsingMessagesSource(fromUserId: String) =
        messagesSource
            .filter { it.fromUserId == fromUserId }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private val infiniteFlow =
    flow<Nothing> {
        while (true) {
            delay(100)
        }
    }

@Test
fun `should start at most one connection`() =
    runTest {
        // given
        var connectionsCounter = 0
        val source =
            infiniteFlow
                .onStart { connectionsCounter++ }
                .onCompletion { connectionsCounter-- }
        val service =
            MessagesService(
                messagesSource = source,
                scope = backgroundScope,
            )

        // when
        service.observeMessages("0")
            .launchIn(backgroundScope)
        service.observeMessages("1")
            .launchIn(backgroundScope)
        service.observeMessages("0")
            .launchIn(backgroundScope)
        service.observeMessages("2")
            .launchIn(backgroundScope)
        delay(1000)

        // then
        assertEquals(1, connectionsCounter)
    }

@Test
fun `should start multiple connections to the source`() =
    runTest {
        // given
        var connectionsCounter = 0
        val source =
            infiniteFlow
                .onStart { connectionsCounter++ }
                .onCompletion { connectionsCounter-- }
        val service =
            MessagesService(
                messagesSource = source,
                scope = backgroundScope,
            )

        // when
        service.observeMessagesUsingMessagesSource("0")
            .launchIn(backgroundScope)
        service.observeMessagesUsingMessagesSource("1")
            .launchIn(backgroundScope)
        service.observeMessagesUsingMessagesSource("0")
            .launchIn(backgroundScope)
        service.observeMessagesUsingMessagesSource("2")
            .launchIn(backgroundScope)
        delay(1000)

        // then
        assertEquals(4, connectionsCounter)
    }

뷰 모델 테스트하기

요약