흔하게 사용되는 예시와 플로우를 테스트할 때 처리해야 할 문제를 살펴보자.

변환함수

Flow를 반환하는 대부분의 함수는 Flow를 반환하는 다른 함수를 호출한다.

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

위 함수의 테스트를 구현하려면 AppointmentRepository를 Fake나 mocking을 해야한다.

data class Appointment(val t1: String, val t2: Instant)

class FakeAppointmentRepository(
    private val flow: Flow<AppointmentsEvent>
) : AppointmentRepository {
    override fun observeAppointments() = flow
}

class ObserveAppointmentsServiceTest {
    val aData1 = Instant.parse("2020-08-30T18:43:00Z")
    val anAppointment1 = Appointment("APP1", aData1)
    val aData2 = Instant.parse("2020-08-31T18:43:00Z")
    val anAppointment2 = Appointment("APP2", aData2)
    
    @Test
    fun `should keep only appoointments from...`() = runTest {
        // given
        val repo = FakeAppointmentRepository(
            flowOf(
                AppointmentsConfirmed,
                AppointmentUpdate(listOf(anAppointment1)),
                AppointmentUpdate(listOf(anAppointment2)),
                AppointmentsConfirmed,
            )
        )
        val service = ObserveAppointmentsService(repo)
        
        // when
        val result = service.observeAppointments().toList()
        
        // then
        assertEquals(
            listOf(
                listOf(anAppointment1),
                listOf(anAppointment2),
            ),
            result
        )
    }
}

세 번째 기능인 ‘5XX 에러 코드를 가진 API 예외가 발생하면 재시도를 해야 한다’는 기능을 생각해보자.

재시도하는 플로우를 반환하는 경우에는, 테스트하는 함수가 무한정 재시도하게 되어 끝나지 않는 플로우가 생성되는데, 이를 테스트하는 가장 쉬운 방법은 take를 사용하여 원소의 수를 제한하는 것이다.

@Test
fun `should retry when API exception...`() = runTest {
    // given
    val repo = FakeAppointmentRepository(
        flow {
            emit(AppointmentUpdate(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
    )
}

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

class MessageService(
    messageSource: Flow<Message>,
    scope: CoroutineScope
) {
    private val source = messagesSource
        .shareIn( // 콜드 플로우를 핫 플로으로 변환하기 위한 확장 함수
            scope = scope,
            started = SharingStarted.WhileSubscribed()
        )

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