Mockk는 어떻게

객체를 mocking 할까

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()
    }
}

참고자료

Made with Slides.com