MVI 패턴에서 효과적으로 테스트 코드 작성하기

Kinda와 함께 작성하는 테스트 코드

kimdohun0104
9 min readOct 18, 2020

배경지식

아래는 글의 내용을 이해하는 데 도움이 되는 배경지식입니다. 아래의 내용을 선행하시면 더 좋은 정보를 얻어가실 수 있으리라 생각합니다.

MVI

MVI 패턴은 Redux, Flux 등과 같은 단방향 아키텍처입니다. MVI는 상태 문제부수효과를 효과적이고 안전하게 다루는 기술로 많은 주목을 받고 있습니다. 자세한 컨셉은 아랫글에서 확인하실 수 있습니다. Kinda를 사용한 예제는 현재와 약간 다를 수 있습니다.

Kinda

Kinda는 MVI 패턴을 효과적으로 구현할 수 있도록 개발한 상태 관리 라이브러리입니다. 모든 Kotlin+Coroutine 기반 프로그램에서 동작할 수 있으며, Android, Kotlin DSL 그리고 이 글에서 자세하게 다룰 Test 등 다양한 추가 기능을 제공합니다.

아키텍처와 테스트 용이성

MVC, MVP, MVVM 관련 자료를 검색해보면 항상 나오는 키워드는 ‘테스트'입니다. 소프트웨어의 복잡성이 증가할수록 테스트의 중요성과 필요성이 강조됩니다. 그래서 패턴을 선택할 때 테스트 용이성 또한 큰 기준으로 자리 잡고 있습니다.

테스트가 가능한 이유

Model과 View의 로직이 하나의 Activity에서 모두 이루어진다면, 테스트 비용이 상당할 것입니다. Activity가 실제로 동작하는 환경에서 테스트가 이루어져야 하며, 이를 코드로 독립적으로 실행 가능한 테스트 케이스를 작성하는 것은 까다롭습니다.

계약에 의한 프로그래밍

MVP 패턴에서는 중간 다리 역할인 Presenter의 등장으로 위 문제를 해결할 수 있습니다. Presenter는 Model과 View 사이의 최소 행위를 보장해주는 방식으로 구현되었으며 (Humble object 패턴), 그 이유로 단위 테스트 작성이 가능해졌습니다. 다양한 상황에 대한 케이스를 쉽고 빠르게 작성할 수 있고, 테스트 실행속도 또한 매우 증가했습니다.

MVVM 패턴은 거기서 한 발 앞으로 나아갔습니다. ViewModel은 View에 대한 의존성이 없는 대신에, View에서 데이터를 관찰(observing)합니다. 그래서 데이터 검증에 더 집중할 수 있습니다.

하지만 아직 개선의 여지가 많습니다. 위 아키텍처 모두 의미 있는 테스트를 작성하는 것은 상당히 힘든 일입니다. 테스트 범위, 대상, 검증 등 프로그래머가 신경 쓸 부분이 많기 때문에, 작성자의 숙련도에 따라서 차이가 크게 날 수 있습니다.

테스트에 유리한 MVI패턴

MVI 패턴의 개념을 처음 접했을 때, 의미 있는 테스트를 쉽게 유도할 수 있는 패턴이라고 생각했습니다. 아래의 그림과 함께 자세히 알아봅시다.

MVI의 상태는 불변성을 가집니다. 상태를 변경하는 유일한 방법은 이벤트를 발생 시켜 새로운 상태를 만드는 것입니다. 중간에 외부 요인으로 인해 데이터가 조작될 수 없으므로, 동일한 상태와 이벤트에 대해서는 항상 같은 결과를 얻을 수 있습니다. 이는 함수형 프로그래밍 패러다임의 순수 함수와 같은 효과를 얻을 수 있습니다. 예측 가능한 데이터를 얻을 수 있으며, 데이터에 대한 추적성을 보장합니다.

// 상태가 S1일 때 E1이 발생한다면, 다음 상태는 반드시 S2이다.
S1 + E1 = S2

이런 이유로 이벤트에 따라 상태가 적절히 변경되었는지 확인하는 것만으로도 의미 있는 테스트를 도출할 수 있습니다. 동시에 개발자가 신경 써야 하는 요소가 많이 줄었기 때문에 숙련도에 따라 결과물이 크게 달라지지 않습니다.

kinda-android-test

Kinda는 1.2.0 버전에서 ‘kinda-android-test’ 라이브러리를 배포했습니다. 기존에 프로그래머가 고려하던 요소를 라이브러리로 책임을 넘기고, 의미 있는 테스트를 작성하는데 집중할 수 있도록 지원하는 것이 주요 목표였습니다. 문제는 구현을 유도하는 방법이었습니다.

중위 함수(infix function)를 발견하다.

그러다 눈에 띈 개념은 코틀린의 중위 함수였습니다. 대표적인 중위 함수의 예로는 to 가 있습니다. to 는 Pair 객체를 만들 때 유용하게 사용되는 함수입니다. 중위 함수에 대해서 익숙하지 않으신 분들은 코틀린의 문법 중 하나라고 오해하기도 합니다.

"left" to "right" // Pair("left", "right")

중위 함수를 활용한 사례를 더 찾아보던 중 ‘Better Android Testing at Airbnb’라는 글을 읽게 되었습니다. 중위 함수와 테스트를 자연스럽게 연결할 수 있는 아이디어를 얻게 되면서 아래와 같이 자리 잡았습니다.

3가지만 기억하세요

색으로 표현된 고려해야할 3요소

Kinda와 함께 테스트를 작성할 때 프로그래머는 3가지 요소만 고려하면 됩니다.

EVENT: 함수를 기준으로 왼쪽엔 발생할 이벤트를 작성합니다. 만약 연속적인 이벤트를 테스트하고 싶다면, Iterable 객체(List, Set 등)를 사용할 수 있습니다.

STATE: 이벤트가 발생한 후 다음 상태를 의미합니다. 익명 함수의 파라미터를 통해서 전달되며, 프로그래머는 이를 통해 단언문(assertion)을 실행할 수 있습니다.

EXPECTED: MVI의 특성상 이벤트가 실행된 후 상태를 예측할 수 있습니다. EXPECTED는 내가 해당 이벤트를 통해서 얻고자 하는 값을 나타냅니다.

처음부터 작성해보는 테스트

간단한 Count 앱의 테스트 코드를 작성해보면서, Kinda와 함께 작성하는 테스트 코드는 얼마나 즐거운지 확인해보겠습니다.

앱의 요구사항은 간단합니다. Increase, decrease 버튼은 각각 count를 증가, 감소시킵니다. 그리고 서버에서 저장된 count 값을 불러오는 기능이 추가될 것입니다.

이 글에서는 기능 구현에 대한 이야기는 다루지 않습니다. 하지만 전체 코드가 궁금하시다면 아래의 깃허브 주소를 참고할 수 있습니다.

KindaViewModelTest

class CountViewModelTest : KindaViewModelTest<CountState, CountEvent, CountSideEffect>() {

Kinda 테스트 라이브러리가 제공하는 기능을 사용하기 위해선 KindaViewModelTest 클래스를 상속받아야 합니다. 이는 반복적인 작업을 수행하거나, 테스트를 위한 메소드, 프로퍼티를 제공해줍니다.

KindaViewModelTest 는 추상 함수인 buildViewModel() 을 가지고 있습니다. buildViewModel()은 각각의 테스트가 실행되기 전 실행되어 상태를 초기화합니다. 그렇기 때문에 이전 테스트 케이스가 결과에 영향을 주지 않습니다.

눈 깜짝할 사이에 테스트 코드를 작성할 준비가 모두 끝났습니다. 테스트를 작성하는 프로그래머는 LiveData(AAC), Mockito에 대한 추가적인 작업에 비용을 크게 투자하지 않아도 테스트 빠르게 환경을 만들 수 있습니다.

테스트 작성

테스트 작성은 정말 간단합니다. 아래는 위에서 설명했던 expectState 함수를 활용한 코드입니다.

@Test
fun `Increase, from count=0 to count=1`() {
CountEvent.Increase expectState { state ->
assertEquals(1, state.count)
}
}

위 방법 말고도 다양하게 테스트를 작성할 수 있습니다. 연속적인 이벤트가 발생한 후 상태를 검증하고 싶다면, 아래의 방법을 사용할 수 있습니다.

@Test
fun `Increase and Decrease, maintain count=0`() {
listOf(Increase, Decrease) expectState { state ->
assertEquals(0, state.count)
}
}

이외에도 currentState 를 통해서 직접 현재 상태에 접근하거나, 이벤트 없이도 expectState 를 사용할 수 있습니다.

Mockito 사용

Count 앱의 요구사항 중, 서버에서 불러온 데이터를 사용하는 경우가 있습니다. 이런 상황엔 어떻게 테스트를 작성할까요?

Kinda 테스트 라이브러리는 현재 Mock 객체를 사용할 땐 Mockito를 사용하는 것을 권장하고 있습니다. 강제하는 것은 아니지만, 앞으로 테스트 라이브러리의 기능은 Mockito를 기반으로 추가될 예정입니다.

@Mock
private lateinit var countRepository: CountRepository
@Test
fun `AttemptRequestCount, receive count=400`() = runBlocking {
`when`(countRepository.requestCount())
.thenReturn(400)

CountEvent.AttemptRequestCount expectState { state ->
assertEquals(400, state.count)
}
}

마무리

Kinda 테스트 라이브러리는 MVI 패턴의 특징을 활용하여 빠르고 쉽게 의미 있는 코드를 작성할 수 있었습니다. 하지만 개선의 여지는 충분히 있으며, 더 발전할 수 있을 것이라 믿습니다. 그러기 위해선 당신의 도움이 필요합니다!

--

--