Mockk는 어떻게
객체를 mocking 할까
Mockk
- kotlin 에서 mock 객체를 만드는 프레임워크
- java mockito 의 kotlin 버전이라고 생각해도 무방
- https://github.com/mockk/mockk
VS mockito
- kotlin DSL 형태 지원
- coroutine 지원
- property, object, nullable 등 kotlin 특화 기능들에 훨씬 좋은 호환성을 보여줌
이 코드의 결과는?
class Simple1 {
fun simple1(): String {
return "hello world"
}
}
class Simple2(
private val simple1: Simple1,
) {
fun simple2(): String {
return simple1.simple1()
}
}이 코드의 결과는?
@Test
fun mocking() {
val simple1 = Simple1()
val simple2 = Simple2(simple1)
every {
simple2.simple2()
} returns "goodbye world"
assertThat(simple2.simple2()).isEqualTo("goodbye world")
} 이 코드의 결과는?
@Test
fun mocking() {
val simple1 = mockk<Simple1>()
val simple2 = Simple2(simple1)
every {
simple2.simple2()
} returns "goodbye world"
assertThat(simple2.simple2()).isEqualTo("goodbye world")
} 이 코드의 결과는?
@Test
fun mocking() {
val simple1 = mockk<Simple1>()
val simple2 = mockk<Simple2>()
every {
simple2.simple2()
} returns "goodbye world"
assertThat(simple2.simple2()).isEqualTo("goodbye world")
} 왜 성공하는가
- mockk 는 일반객체마저 mocking?
- every 내에서 mocking 대상 객체를 기반으로 리플렉션을 사용
- simple2 가 mock 객체가 아님을 판별하고 객체 구조를 따라올라가 simple1 을 찾아 mockking
fun find_mock_object(target: Any): Any {
var temp: Any = target
while(true) {
val isMock: Boolean = isMockKMock(temp)
if(isMock) {
return target
}
val field: Field = temp.javaClass.declaredFields[0]
field.trySetAccessible()
temp = field.get(temp)
}
} 왜 성공하는가
- 객체 구조가 복잡할땐?
- 메서드 내부 코드까지 분석하는건 불가능
- mockk 코드 내부에 각종 디버깅을 걸고 분석했지만 Simple2 에 대해 break point 가 걸리는 경우는 없었음
Mockk 가 Mock 객체를 만드는 방법
- 일반적으로 mock 객체란 실제 객체한테 전달되는 호출을 가로채서 다른 응답을 주는 객체를 의미
- 이럴때 사용되는 대표적인 디자인패턴이 proxy pattern
- proxy pattern 을 구현하는 가장 대표적인 방법은 상속을 이용
class ProxySimple1(
private val simple1: Simple1,
) : Simple1() {
override fun simple1(): String {
return "goodbye world"
}
}Mockk 가 Mock 객체를 만드는 방법
- 하지만 kotlin 은 기본적인으로 상속을 막음
- 상속을 이용한 proxy 구현 대신 바이트 코드를 삽입하는 방식으로 proxy 구현
- https://bytebuddy.net
- system property 를 이용해 mockk 가 만드는 mock 클래스를 확인할 수 있음
tasks.withType<Test> {
useJUnitPlatform()
systemProperty("io.mockk.classdump.path", "output")
}Mockk 가 Mock 객체를 만드는 방법
public final class Simple1 {
public final String simple1() {
Callable var10000;
label40: {
if (this.getClass() == HashMap.class) {
if ((new Object[0]).length == 1 && (new Object[0])[0] == HashMap.class) {
var10000 = null;
break label40;
}
if ((new Object[0]).length == 2 && (new Object[0])[1] == HashMap.class) {
var10000 = null;
break label40;
}
}
JvmMockKDispatcher var1 = JvmMockKDispatcher.get(-7627940941739126697L, this);
var10000 = var1 != null && var1.isMock(this) ?
var1.handler(this, Simple1.class.getMethod("simple1"), new Object[0]) : null;
}
Callable var4 = var10000;
String var2 = var4 != null ? null : "hello world";
if (var4 != null) {
var2 = (String)var4.call();
}
return var2;
}
}
Mockk 가 Mock 객체를 만드는 방법
- mock 객체는 어떠한 command 객체가 객체 구조를 판별하며 mocking 을 진행하는게 아님
- every 에 람다로 전달된 함수를 실행시키는 시점에 mock 객체에 삽입된 코드를 이용하여 스스로 mocking 을 진행함
- 그래서 break point 에 Simple2 가 잡히는 케이스가 없음(Simple2 는 실제 객체로 삽입된 바이트 코드가 없기때문)
- JvmMockKDispatcher 내에서 DISPATCHER_MAP 이라는 static field 로 관리하기때문에 mock 객체의 전체적인 상태를 관리함(verify 같은 assertion 사용가능)
Mockk 가 Mock 객체를 만드는 방법
- 그래서 이런코드는 컴파일은 되지만 런타임에 예외가 발생함
class Simple1 {
fun simple1(): Int {
return 999
}
}
class Simple2(
private val simple1: Simple1,
) {
fun simple2(): String {
return simple1.simple1().toString()
}
}참고자료
Mockk는 어떻게 객체를 mocking 할까
By changyong
Mockk는 어떻게 객체를 mocking 할까
- 215