안녕하세요.
오늘은 코틀린에서 사용하는 모킹 프레임워크, MockK에 대해 알아보도록 하겠습니다.
MockK?
Mock + Kotlin의 합성어 입니다.
Mock
Mock 객체는 실제 객체를 대신하여 가짜 객체를 불러온 뒤, 특정 동작을 지정할 수 있는데 사용됩니다.
이러한 행위를 모킹 (mocking) 이라고 합니다.
의의
실제 객체의 의존성을 제거하고, 테스트를 독립적이고 쉽게 제어할 수 있는 기술입니다.
아래는 의존성이 있는 클래스 예시입니다.
class Server {
fun getGrade(user: User): String {
return when {
user.getScore() >= 90 -> "A"
user.getScore() >= 80 -> "B"
user.getScore() >= 70 -> "C"
else -> "D"
}
}
}
의존성이 있는 Server 클래스에서 Test 할 시에 아래의 문제점이 존재합니다.
- user 클래스의 덩치가 크다면, 유닛 테스트 시에 부담이 큽니다.
- user 클래스가 다른 클래스와 연결되어 있다면, 독립적인 유닛 테스트라 보기 어렵습니다.
이런 상황에서 Mock을 사용해 이러한 문제점을 극복할 수 있습니다.
vs Mockito
기존 Java 언어에서는 Mockito 테스트 프레임워크를 많이 사용했습니다.
MockK는 Mockito 보다 제공해주는 코틀린 문법이 많아 Kotlin 언어에서 많이 쓰이는 테스트 프레임워크 입니다.
의존성 추가
build.gradle (app 수준) 에서 아래와 같이 추가해 줍니다.
ext.mockkVersion = '1.13.5'
dependencies {
...
// MockK
testImplementation("io.mockk:mockk:${mockkVersion}")
}
Mock
의존성이 있는 클래스로 아래와 같이 예시를 들어보겠습니다.
class User {
fun getScore(): Int {
val scores = listOf(90, 80, 70, 60)
return scores.random()
}
}
// =========================================
class Server {
fun getGrade(user: User): String {
return when {
user.getScore() >= 90 -> "A"
user.getScore() >= 80 -> "B"
user.getScore() >= 70 -> "C"
user.getScore() >= 60 -> "D"
else -> {
Log.e(TAG, "불가능한 값")
"F"
}
}
}
companion object {
private val TAG = "Server"
}
}
이를 유닛 테스트 해보겠습니다.
Step 1. 변수 선언 및 초기화
mock 객체를 가져와 초기화하는 방법으로 2가지 중 하나를 사용할 수 있습니다.
1) 직접 초기화 해주기
class ServerTest {
private val mockUser = mockk<User>()
private val server = Server(mockUser)
}
2) 어노테이션으로 초기화 해주기
@MockK : 모의 객체를 생성하고 초기화할 수 있습니다.
@InjectMockK : 실제 클래스의 인스턴스를 생성하고, 모의 객체를 주입합니다.
class ServerTest {
@MockK
private lateinit var mockUser: User
@InjectMockKs
private lateinit var server: Server
}
지금 포스팅에서는 어노테이션으로 초기화하는 방법을 사용해 설명하겠습니다.
Step 2. before, after 단계
모든 테스트가 끝날 때마다 수행해 줄 코드를 지정해줍니다.
- MockKAnnotations.init(this)
- 위에서 어노테이션을 사용했습니다. 이를 제대로 동작시키기 위해서 필요한 코드입니다.
- unmockkStatic
- static class를 모킹했다면, 다른 클래스에 영향을 주지 않기 위해 unmock이 반드시 필요합니다.
- mockkStatic
- java static class를 모킹하기 위해 필요한 코드입니다. Log 클래스가 호출되었는지 여부를 확인하기 위해 필요한 부분입니다.
(mockkStatic, mockkObject에 대해서는 다음 포스팅에서 다룰 예정입니다)
class ServerTest {
...
@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
mockkStatic(Log::class)
}
@AfterEach
fun tearDown() {
unmockkStatic(Log::class)
}
}
Step 3. Test
mock 객체를 스터빙해서 의도한 결과대로 나오는지 테스트 합니다.
- every
- mockUser의 getScore() 메서드 반환 값을 stubbing 해줍니다.
- 함수를 실제로 호출해서 반환 값을 얻는 것이 아닌, 내가 지정한 값이 반환되게 됩니다.
- assert
- 실제 객체의 메서드를 호출해서 의도한 결과대로 나오는지 확인합니다.
- verify
- 해당 함수가 호출 되었는지 확인합니다.
- Log.e 메서드를 verify 안에 작성하기 위해선 모킹 클래스여야 합니다. 그래서 위에 mockkStatic으로 로그 클래스를 모킹 시켜줬습니다.
- verifyOrder
- 함수 호출 순서를 검증할 수 있습니다.
class ServerTest {
...
@ParameterizedTest
@CsvSource(
"95, A",
"90, A",
"84, B",
"80, B",
"70, C",
"60, D"
)
fun `get grade when score is correct`(score: Int, grade: String) {
every { mockUser.getScore() } returns score
val result = server.getGrade(mockUser)
assertEquals(grade, result)
verify {
mockUser.getScore()
}
}
@Test
fun `get grade when score is not correct`() {
every { mockUser.getScore() } returns 50
every { Log.e(any(), any()) } returns 0
val result = server.getGrade(mockUser)
assertEquals("F", result)
verifyOrder {
mockUser.getScore()
Log.e(any(), any())
}
}
}
전체 코드입니다.
import android.util.Log
import io.mockk.*
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class ServerTest {
@MockK
private lateinit var mockUser: User
@InjectMockKs
private lateinit var server: Server
@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
mockkStatic(Log::class)
}
@AfterEach
fun tearDown() {
unmockkStatic(Log::class)
}
@ParameterizedTest
@CsvSource(
"95, A",
"90, A",
"84, B",
"80, B",
"70, C",
"60, D"
)
fun `get grade when score is correct`(score: Int, grade: String) {
every { mockUser.getScore() } returns score
val result = server.getGrade(mockUser)
assertEquals(grade, result)
verify {
mockUser.getScore()
}
}
@Test
fun `get grade when score is not correct`() {
every { mockUser.getScore() } returns 50
every { Log.e(any(), any()) } returns 0
val result = server.getGrade(mockUser)
assertEquals("F", result)
verifyOrder {
mockUser.getScore()
Log.e(any(), any())
}
}
}
모의 객체 반환
위에서 every로 값을 지정해줬습니다.
값 뿐만 아니라 객체를 지정해줄 수도 있습니다.
아래처럼 코드가 작성되었다고 해봅시다.
class Server {
fun getGrade(): String {
val user = getUser()
return when {
user.getScore() >= 90 -> "A"
user.getScore() >= 80 -> "B"
user.getScore() >= 70 -> "C"
user.getScore() >= 60 -> "D"
else -> {
Log.e(TAG, "불가능한 값")
"F"
}
}
}
fun getUser(): User {
return User()
}
companion object {
private val TAG = "Server"
}
}
Server 클래스의 getUser() 메서드를 스터빙해서 원하는 객체로 반환시킬 수 있습니다.
getUser 메서드를 스터빙하고, getGrade 메서드는 실제 함수를 부르기 위해 spy 객체를 선언합니다.
class ServerTest {
@MockK
private lateinit var mockUser: User
private val spyServer = spyk<Server>()
...
@Test
fun `get grade when score is not correct`() {
every { spyServer.getUser() } returns mockUser
every { mockUser.getScore() } returns 50
every { Log.e(any(), any()) } returns 0
val result = spyServer.getGrade()
assertEquals("F", result)
verifyOrder {
mockUser.getScore()
Log.e(any(), any())
}
}
}
Spy
mock 객체는 가짜 객체였다면,
spy 객체는 진짜 객체 + 일부 스터빙 입니다.
- 아무런 스터빙을 하지 않으면 원래 설정된 값을 반환합니다.
- 특정 값으로 스터빙하면 스터빙 한 값이 반환됩니다.
이렇듯 진짜 객체 + 일부 스터빙을 하고 싶을 때 spy 객체를 사용합니다.
Server의 메서드로 아래와 같이 2개가 있고, getUser만 스터빙하고 싶을 때 spy를 사용하면 됩니다.
class Server {
fun getGrade(): String {
...
}
fun getUser(): User {
...
}
}
class ServerTest {
private val spyServer = spyk<Server>()
...
fun `get grade when score is correct`(score: Int, grade: String) {
every { spyServer.getUser() } returns mockUser
...
val result = spyServer.getGrade()
...
}
}
'Unit Test' 카테고리의 다른 글
[Unit Test] MockK (2) - mockkObject, mockkStatic, mockkConstructor (0) | 2024.09.30 |
---|---|
[Unit Test] JUnit5 (2) - Parameterized Test (0) | 2024.07.08 |
[Unit Test] JUnit5 (1) - Use in Android Studio (1) | 2024.06.29 |