코루틴 Unit Test

ganghee·2022년 2월 25일
0

Test

목록 보기
4/5
post-thumbnail

코루틴을 테스트 하려면 먼저 디펜던시 설정을 해야합니다.

testImplementation = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0"

JUnit5에서 테스트하려면 다음과 같은 Extension 클래스를 설정합니다. 코루틴은 Dispachers를 설정해서 테스트 쓰레드에서 코드가 돌게 해야합니다. 그렇지 않으면 다음과 같은 에러 메시지가 나오게 됩니다.

Exception in thread "main @coroutine#1" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used

1. 코루틴 테스트 스레드 설정

테스트 패키지에 다음과 같은 클래스를 구현합니다.

@ExperimentalCoroutinesApi
class CoroutinesTestExtension(
    private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : BeforeEachCallback, AfterEachCallback {

    override fun beforeEach(context: ExtensionContext?) {
        Dispatchers.setMain(dispatcher)
    }

    override fun afterEach(context: ExtensionContext?) {
        Dispatchers.resetMain()
    }
}

2. 테스트 클래스 만들기

계산기 어플을 테스트 해보겠습니다.
구조는 CalculatorViewMedel과 매개변수로 계산 작업을 하는 Expression 객체, 데이터를 서버나 로컬에서 가져하는 CalculatorRepository객체가 있습니다. 테스트 코드 설정은 다음과 같이 해줍니다.

@ExperimentalCoroutinesApi
@ExtendWith(CoroutinesTestExtension::class)
class CalculatorViewModelTest {

    private lateinit var calculatorViewModel: CalculatorViewModel

    @BeforeEach
    fun setUp() {
        val repository: CalculatorRepository = mockk(relaxed = true)
        calculatorViewModel =
            CalculatorViewModel(ExpressionInjector.providesExpression(), repository)
    }
	...
}

1에서 작성했던 클래스를 @ExtendWith를 통하여 설정해줍니다. 그리고 setUp()함수 안에 repository는 mock객체로 만들어줍니다. viewModel객체가 CalculatorRepository객체를 의존하고 있기 때문에 이를 mock객체로 만들어 줘야 합니다.
참고로 CalculatorViewModel 생성자에 Expression과 CalculatorRepository가 주입되었는데 이렇게 한 이유는 CalculatorViewModel이 생성이 되었을 때 어떤 객체들을 의존하고 있는지 생성시점에 알 수 있지 때문입니다. 만약 이 두 객체가 CalculatorViewModel의 맴버 변수로 있게 되면은 클라이언트에서 어떤 객체를 의존하고 있는지 알 수 없어 테스트하기 어려워집니다.

3. 테스트 함수 작성

@Test
    fun `수식 1 을 입력하면 1 이 보여야한다`() {
        // WHEN
        calculatorViewModel.appendOperand(1)

        // THEN
        assertThat(calculatorViewModel.statement.value).isEqualTo("1")
    }

위의 statement변수는 StateFlow 타입을 가지고 있습니다. 그러면 SharedFlow 타입은 어떻게 할까요?

먼저 StateFlowSharedFlow의 특징을 살펴봅시다.
이 두가지의 Flow는 Hot Flow형식으로 collector가 없어도 데이터가 계속 발행되는 특징을 가지고 있습니다.
StateFlow는 초기값을 가지고 있으며 발행하는 데이터가 바뀔때만 collect된다는 특징이 있습니다. 그리고 현재 값을 가져올 수 있습니다.
SharedFlow는 onBufferOverflow 매개변수를 통해 오버플로우를 설정하고 replay를 통해 값을 저장하다가 새로운 collector에게 내보낼 수 있습니다. 그리고 SingleLiveEvent를 대체할 수 있니다. 테스트 할 때는 값을 가져올 수 없어 스코프안에서 확인을 해줘야합니다. 이를 위해 Turbine이라는 라이브러리를 사용했습니다.

Turbine라이브러리는 Flow 테스트를 쉽고 간결하게 만들어 줍니다. Flow.test{ ... } 형식으로 사용하면 됩니다. 다음과 같이 코드를 만들 수 있습니다.

 @Test
    fun `빈 수식 입력시 예외처리를 해야한다`() = runBlocking {
        // WHEN
        val job = launch(start = CoroutineStart.LAZY) {
            calculatorViewModel.calculateStatement()
        }

        // THEN
        calculatorViewModel.errorMessage.test {
            job.start()
            assertEquals(IS_NOT_OR_BLANK, awaitItem())
        }
    }

전체 코드는 여기서 확인할 수 있습니다.

0개의 댓글