코루틴을 테스트 하려면 먼저 디펜던시 설정을 해야합니다.
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
테스트 패키지에 다음과 같은 클래스를 구현합니다.
@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()
}
}
계산기 어플을 테스트 해보겠습니다.
구조는 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의 맴버 변수로 있게 되면은 클라이언트에서 어떤 객체를 의존하고 있는지 알 수 없어 테스트하기 어려워집니다.
@Test
fun `수식 1 을 입력하면 1 이 보여야한다`() {
// WHEN
calculatorViewModel.appendOperand(1)
// THEN
assertThat(calculatorViewModel.statement.value).isEqualTo("1")
}
위의 statement변수는 StateFlow 타입을 가지고 있습니다. 그러면 SharedFlow 타입은 어떻게 할까요?
먼저 StateFlow와 SharedFlow의 특징을 살펴봅시다.
이 두가지의 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())
}
}
전체 코드는 여기서 확인할 수 있습니다.