이전 포스팅에서 설명한 MockK 설정이나 간단한 사용법을 알고싶다면 이전 포스팅을 참고하자.
이번 포스팅에선 MockK에서 mock 객체를 선언하는 여러 메소드들을 아주 자세하게 알아보려고 한다.
테스트 환경 및 방법
JUnit 5 + MockK 조합으로 테스트를 작성한다.
아래 라이브러리 의존성을 추가해주자.
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.slf4j:slf4j-api:2.0.7")
testImplementation("io.mockk:mockk:1.13.5")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.slf4j:slf4j-simple:2.0.7")
JUnit 5에 MockK를 사용하려면 @ExtendWith(MockKextension::class) 어노테이션을 선언해줘야 한다.
@ExtendWith(MockKExtension::class) << 필수
class MockkTest { ... }
mockk<T>
기본적인 mock 객체를 만들 때 사용하는 메소드
아래 UserService로 설명을 하겠다.
data class User(
val id: Long,
val type: Type,
val name: String? = "default",
val address: Address,
val phones: List<String>,
val active: Boolean
) {
data class Address(
val zipCode: String,
val basicAddress: String,
val detailAddress: String
)
enum class Type {
ADMIN,
USER
}
}
class UserService {
fun getUser(): User? {
return User(
id = 1,
type = User.Type.ADMIN,
name = "이름",
address = User.Address(
zipCode = "12345",
basicAddress = "기본 주소",
detailAddress = "상세 주소"
),
phones = listOf(
"01012345678",
"01098765432"
),
active = true
)
}
fun updateUser() {}
}
UserService를 mockk를 이용해서 mock 객체로 만들어보자.
private val userService = mockk<UserService>()
위처럼 작성할 수도 있고 annotation을 붙이는 방법도 있다.
@MockK
lateinit var userService: UserService
첫 번째 방법을 선호한다. 깔끔하고 라인 수도 더 잡아 먹어서 보기 좋다.
파라미터
mockk는 name, relaxed, relaxUnitFun, moreInterfaces, block 총 5개의 파라미터를 가지고 있는데 하나씩 알아보자.
name
mockk 객체의 이름을 설정할 때 사용한다.
private val userService = mockk<UserService>(name = "userService")
mockk 객체의 이름은 여러군데에서 사용되는데 그건 추후에 하나씩 등장할 예정이다.
만약, name을 별도로 설정하지 않으면 mockk<클래스명>(번호)로 지어진다. 위 예제에선 mockk<UserService>(1)으로 지어진다.
relaxed
스터빙 하지 않은 mock 객체의 메소드를 호출할 때 에러를 발생시키지 않는다는 옵션이다.
아래에서 자세히 알아보자.
위에서 mock 객체로 선언한 UserService의 getUser() 메소드를 호출해보자.
@ExtendWith(MockKExtension::class)
class MockkTest {
private val userService = mockk<UserService>()
@Test
fun testMockk() {
userService.getUser()
}
}
getUser()의 answer 값이 없다고 에러를 뱉는다.
no answer found for UserService(#1).getUser() among the configured answers: ()
그렇다. mockk으로 만든 mock 객체의 메소드들은 별도 스터빙을 하지 않고 호출하면 에러를 발생시킨다.
스터빙 하지 않은 메소드를 호출할 때 기본으로 에러를 발생시키는 게 사용하다보면 너무 불편한다.
굳이 호출하는 메소드의 리턴 값을 지정해줄 필요없는 경우가 많기 때문이다.
이럴 때 relaxed를 true로 주면 된다.
private val userService = mockk<UserService>(relaxed = true)
relaxed를 붙여서 다시 실행해보자. 에러가 발생하지 않는 걸 볼 수 있다.
음 그럼 스터빙을 하지도 않았는데 리턴은 어떤 값이 되는 거지?
@ExtendWith(MockKExtension::class)
class MockkTest {
private val userService = mockk<UserService>(relaxed = true)
@Test
fun testMockk() {
with(userService.getUser()) {
println("Object: $this")
println("Long: $id")
println("Enum: $type")
println("String: $name")
println("Object: $address")
println("List: $phones")
println("Boolean: $active")
}
println("Unit: ${userService.updateUser()}")
}
}
Object | 기본값들로 채워진 해당 타입의 객체 |
Long | 0 |
Float, Double | 0.0f |
Enum | 해당 타입의 Enum (ordinal: 0, name: "") |
String | "" |
List, Map 등 | 빈 컬렉션 |
Boolean | false |
Unit | Unit |
Object나 Enum은 child^2 of #1#2 같이 이상한 이름으로 찍힌다.
relaxUnitFun
리턴 값이 Unit인 fun은 스터빙 하지 않아도 호출 시 에러 발생 시키지 않음
relaxed는 리턴 값에 상관없이 전부 적용되는 반면, relaxUnitFun은 리턴 값이 Unit인 메소드에만 적용된다.
@ExtendWith(MockKExtension::class)
class MockkTest {
private val userService = mockk<UserService>(relaxUnitFun = true)
@Test
fun testMockk() {
assertThrows<MockKException> { userService.getUser() }
assertDoesNotThrow { userService.updateUser() }
}
}
리턴 값이 Unit이 아닌 getUser는 에러가 발생하고, 리턴 값이 Unit인 updateUser는 에러가 발생하지 않는다.
언제 써야 하는지 아직 감이 잘 오지 않는다.
moreInterfaces
mock 객체에 interface를 구현 시키는 방법이라는데 정확하게 어떻게 쓰고 어떨 때 사용하는지 모르겠다..
찾게되면 다시 업데이트 하겠다.
block
mock 객체 생성 후 바로 실행될 작업
@ExtendWith(MockKExtension::class)
class MockkTest {
private val userService = mockk<UserService> {
every { getUser() } returns null
}
@Test
fun testMockk() {
assertEquals(null, userService.getUser())
}
}
userService가 생성되자마자 getUser()를 null로 반환하라는 스터빙을 선언했다.
실무에서 저걸 써본 적은 없지만 테스트에서 공통으로 사용하는 스터빙이 있는 경우 사용하면 될 거 같다.
spyk<T>, spyk(obj)
spy는 스터빙하지 않은 것은 실제 객체와 동일하게 동작한다.
mockk은 전부를 mocking하는 반면에, spy는 스터빙하지 않은 것들에 대해선 mocking을 하지 않는다.
class Calculator {
fun plus(a: Int, b: Int): Int {
return privatePlus(a, b)
}
private fun privatePlus(a: Int, b: Int): Int {
return a - b
}
}
위 Calculator를 spy 객체로 만들어보자.
private val calculator = spyk<Calculator>()
@SpyK
var calculator: Calculator = Calculator()
spyk<T>()나 @SpyK를 사용해서 spy 객체를 만들 수 있다.
annotation을 사용하는 방법은 mockk와 조금 다르다. lateinit var를 사용하지 못 한다. var를 사용해야 한다.
특이하게 한 가지 방법이 더 존재한다.
private val calculator = spyk(Calculator())
전자 방법으로 만들면 기본 생성자를 사용한다.
후자 방법은 다른 생성자를 사용하고 싶거나 생성자에 필요한 값들을 넣고 싶을 때 사용하면 된다.
@ExtendWith(MockKExtension::class)
class SpyKTest {
private val calculator = spyk<Calculator>()
@Test
fun testSpyK() {
assertEquals(3, calculator.plus(1, 2))
}
}
spy 객체로 만든 caculator의 plus를 호출해보자.
어떻게 될까?
성공이다.
spy 객체는 별도 스터빙을 하지 않아도 mockk 처럼 에러가 발생하지 않는다.
파라미터
spyk는 name, moreInterfaces, recordPrivateCalls, block 총 4개 파라미터를 가지고 있다.
(spyk(obj)는 obj 파라미터가 하나 더 있다.)
mockk에서 설명한 파라미터들은 설명을 생략한다.
recordPrivateCalls
이름 그대로 private 메소드 호출을 record 할거냐는 의미이다.
true로 주면 private 메소드를 verify 할 수 있다.
private val calculator = spyk<Calculator>(recordPrivateCalls = true)
private method 스터빙 방법은 특이하다.
private 메소드 signature를 반드시 맞춰줘야 한다.
every { spykObject["private 메소드명"](private 메소드 signature) } returns 값
실제 private 메소드가 1번 호출됐는지 verify 해보자.
@ExtendWith(MockKExtension::class)
class SpyKTest {
private val calculator = spyk<Calculator>(recordPrivateCalls = true)
@Test
fun testSpyK() {
every { calculator["privatePlus"](1, 2) } returns 50
assertEquals(50, calculator.plus(1, 2))
verify(exactly = 1) { calculator["privatePlus"](1, 2) }
}
}
privatePlus 메소드가 1번 호출된 게 검증 잘 됐다.
만약, recordPrivateCalls를 설정 안 하고 private 메소드를 스터빙하면?
@ExtendWith(MockKExtension::class)
class SpyKTest {
private val calculator = spyk<Calculator>(recordPrivateCalls = false)
@Test
fun testSpyK() {
every { calculator["privatePlus"](1, 2) } returns 50
assertEquals(50, calculator.plus(1, 2))
verify(exactly = 1) { calculator["privatePlus"](1, 2) }
}
}
호출되지 않았다고 에러가 발생한다.
mockk는 어떻게 되나 테스트를 해봤는데?
@ExtendWith(MockKExtension::class)
class MockkWithPrivateTest {
private val calculator = mockk<Calculator>(relaxed = true)
@Test
fun testMockkWithPrivate() {
every { calculator.plus(any(), any()) } answers { callOriginal() } // 실제 작동하게 스터빙
every { calculator["privatePlus"](1, 2) } returns 50
assertEquals(50, calculator.plus(1, 2))
verify(exactly = 1) { calculator["privatePlus"](1, 2) }
}
}
? 성공했다. 뭐지..
mockk는 private 메소드도 기본적으로 record하고 있는 걸로 보인다.
callOriginal을 이용해서 spy 객체처럼 작동하게 했더니 이게 되네..;;
Mock 객체 생성 메소드를 한번에 다 정리하려고 했는데 mockk하고 spyk 2개 정리했더니 너무 길어져서 다음 포스팅에 mockkClass, mockkObject, mockkStatic, mockkConstructor를 정리하는 걸로...
댓글