프로젝트를 진행하며 테스트 코드의 중요성은 잘 알고 있었지만, 테스트 코드를 작성하는 게 익숙지 않기도 하고 일정이 빠듯하다는 핑계로 테스트 코드를 작성하지 않았었다. 언제까지 미룰 순 없어서 테스트 코드 작성법에 대하여 공부하게 되었고, MockK 라이브러리를 사용하여 테스트 코드를 작성하는 법을 알게 되었다.
MockK
테스트 코드 작성 시 mock 처리를 위해 Java에서는 Mockito를 많이 사용한다. Kotlin에서는 Mockito와 유사한 MockK라는 라이브러리가 존재한다. Mockito와 사용법이 유사하여 조금만 노력하면 쉽게 적응할 수 있다.
Dependency
MockK를 사용하기 위해선 dependency 추가가 필요하다. 이 글을 쓰고 있는 현재의 최신 버전은 1.10.1이다.
testImplementation "io.mockk:mockk:$mockk_version"
androidTestImplementation "io.mockk:mockk-android:$mockk_version"
Mock 테스트 예제
SampleViewModel.kt
class SampleViewModel(private val repository: SampleRepository) {
fun insert(sample: Sample) {
repository.insert(sample)
}
}
SampleRepository.kt
interface SampleRepository {
fun insert(sample: Sample)
}
Sample.kt
data class Sample(
val value: String
)
MVVM 패턴의 ViewModel을 테스트한다고 가정해보자. Sample 객체를 insert() 하는 테스트 코드를 작성한다.
class SampleViewModelTest {
@Test
fun test01_insert() {
val sample = mockk<Sample>()
val repository: SampleRepository = mockk()
val viewModel = SampleViewModel(repository)
viewModel.insert(sample)
verify { repository.insert(sample) }
}
}
ViewModel의 생성자로 Repository를 주입한다. 이때 Repository는 ViewModel 테스트의 범주에서 벗어나 있으므로 mock 객체로 만들어서 주입해준다.
ViewModel의 insert() 함수에서 repository.insert() 함수를 호출하고 있기 때문에, verify() 함수로 repository.insert() 함수가 호출되고 있는지 검사한다. verify() 함수는 호출 여부를 테스트할 수 있다.
io.mockk.MockKException: no answer found for: SampleRepository(#2).insert(Sample(#1))
앞서 작성한 테스트 코드를 실행하면 오류가 발생하고 위와 같은 메시지를 보여준다. SampleRepository는 mock 객체로 만들어졌기 때문에 insert()가 어떤 결과를 반환해야 할지 몰라서 생기는 오류다. 이럴 때는 every() 함수를 사용해서 insert() 함수가 어떤 값을 반환해야 하는지 정의해줄 수 있다.
class SampleViewModelTest {
@Test
fun test01_insert() {
val sample = mockk<Sample>()
val repository: SampleRepository = mockk()
val viewModel = SampleViewModel(repository)
every { repository.insert(sample) } just Runs
viewModel.insert(sample)
verify { repository.insert(sample) }
}
}
repository.insert()는 반환 값이 없는 함수이기 때문에 just Runs로 설정해주면 verify를 통과할 수 있게 된다.
여러 가지 함수들
위의 예제에서 간단한 ViewModel 테스트 코드를 작성해보았다. 아래에서는 테스트 코드를 작성할 때 자주 사용하는 함수들에 대해서 알아볼 것이다.
1. every()
every() 함수는 위의 예제에서 봤듯이 mock 객체가 어떻게 동작할지 정의하는 함수다. returns 함수로 특정 객체를 반환시켜줄 수 있고, throws로 익셉션을 발생시킬 수도 있다.
@Test
fun test() {
val sample = mockk<Sample>()
every { repository.insert(sample) } just Runs
every { repository.insert(sample) } throws Exception()
every { repository.getSample() } returns mockk()
every { repository.getSample() } answers {
println("call repository.getSample()")
sample
}
}
2. mockk(relaxed = true)
위의 예제에서 every() 함수를 정의하지 않아서 오류가 생겼었다. 매번 every() 함수로 지정해주기 귀찮다면 mock 객체를 만들 때 relaxed 파라미터 값을 true로 설정하여 relaxed mock 객체를 사용하는 것이 좋다. relaxed mock 객체의 메서드를 호출하면 0, false, ""과 같은 기본값을 반환하고 참조 타입인 경우에는 다시 relaxed mock 객체를 반환한다.
class SampleViewModelTest {
@Test
fun test01_insert() {
val sample = mockk<Sample>()
val repository: SampleRepository = mockk(relaxed = true)
val viewModel = SampleViewModel(repository)
viewModel.insert(sample)
verify { repository.insert(sample) }
}
}
3. any()
만약 mock 객체의 메서드를 호출하는데 파라미터로 private 변수를 넣어주고 있다면 어떻게 테스트할 수 있을까? 이럴 때는 any() 함수를 사용하여 처리할 수 있다. 임의의 인자 값과 일치하도록 설정해주는 역할을 한다.
class SampleViewModelTest {
@Test
fun test01_insert() {
val sample = mockk<Sample>()
val repository: SampleRepository = mockk(relaxed = true)
val viewModel = SampleViewModel(repository)
viewModel.insert(sample)
verify { repository.insert(any()) }
}
}
4. capture()
파라미터를 캡처하고 싶을 땐 capture() 함수를 사용한다.
class SampleViewModelTest {
private lateinit var viewModel: SampleViewModel
private val repository: SampleRepository = mockk(relaxed = true)
@Before
fun before() {
viewModel = SampleViewModel(repository)
}
@Test
fun test01_insert() {
val sample = mockk<Sample> {
every { value } returns "sample"
}
val slot = slot<Sample>()
every { repository.insert(capture(slot)) } just Runs
viewModel.insert(sample)
val argument = slot.captured
assert(argument.value == "sample")
}
}
5. @MockK
앞에서 mock 객체를 만들 때는 mockk() 함수를 사용했다. 이 또한 귀찮다면 Annotation을 사용하여 mock 객체를 생성할 수 있다.
abstract class UnitTest {
@Rule
@JvmField
val injectMocks = TestRule { statement, _ ->
MockKAnnotations.init(this, relaxUnitFun = true)
statement
}
}
class SampleViewModelTest : UnitTest() {
private lateinit var viewModel: SampleViewModel
@MockK
private lateinit var repository: SampleRepository
@Before
fun before() {
viewModel = SampleViewModel(repository)
}
@Test
fun test01_insert() {
val sample = mockk<Sample>()
viewModel.insert(sample)
verify { repository.insert(sample) }
}
}
UnitTest라는 abstract class를 만든 뒤, 이를 상속하면 @MockK로 간단하게 mock 객체를 생성할 수 있다.
6. spyk()
아래 Sample의 getString() 함수를 테스트한다고 해보자.
data class Sample(
val value: String
) {
fun getString() = value + "Test"
}
data class가 간단한 경우에는 아래와 같이 직접 객체를 만들어서 테스트할 수 있을 것이다.
@Test
fun sample_test() {
val sample = Sample("sample")
val result = sample.getString()
assert(result == "sampleTest")
}
하지만 직접 넣어줘야 하는 파라미터가 많은 class인 경우엔 직접 생성하는 게 번거로울 것이다. Sample 객체를 mock 객체로 만들어서 테스트를 해보자.
@Test
fun sample_test() {
val sample = mockk<Sample> {
every { value } returns "sample"
}
val result = sample.getString()
assert(result == "sampleTest")
}
io.mockk.MockKException: no answer found for: Sample(#2).getString()
Sample 객체를 mock 객체로 만들어서 every() 함수를 사용해 value를 정의해줬다. 그 뒤 getString() 메서드를 호출해 값을 비교한다. 하지만 위와 같은 오류가 발생하게 된다. 이는 every() 함수로 value 값만 정의해주고 getString() 메서드는 정의해주지 않았기 때문이다.
이럴 때는 spyk() 함수를 사용하면 간단하게 테스트해볼 수 있다.
@Test
fun sample_test() {
val sample = mockk<Sample> {
every { value } returns "sample"
}
val result = spyk(sample).getString()
assert(result == "sampleTest")
}
코틀린에서 사용하는 MockK라는 라이브러리에 대해 살펴보았다. 글에서 설명한 기능 외에도 다양한 기능이 더 있으니 필요하면 공식 사이트를 참고해보자.
'Android' 카테고리의 다른 글
[Android] Multi Module로 Android project 구성하기 (0) | 2021.06.20 |
---|---|
[Android] Event Wrapper를 사용한 단일 이벤트 처리 (1) | 2021.06.20 |
[Android] Sticky Header RecyclerView 응용하기 (0) | 2021.04.19 |
[Android] LeakCanary로 메모리릭 잡기 (0) | 2021.04.17 |
[Android] 문자열 단/복수 처리 - plurals (0) | 2021.03.28 |