카테고리 없음

PowerMock 알아보기

봄석 2019. 10. 29. 17:00

PowerMock 알아보기

Mockito가 지원하는 기능은 간단한 기능의 유닛 테스트에는 충분하지만 코드 구조가 복잡할 경우 테스트하기에 힘든 부분이 있습니다.

혹은 반대로, 테스트를 위해서 좋은 코드 구조를 포기해야만 하는 경우도 있습니다.

PowerMock은 그런 문제들을 피해 유닛 테스트를 할 수 있게 도와줍니다.

PowerMock 시작하기

PowerMock은 두 개의 확장 API로 구성됩니다.

하나는 EasyMOckMockito입니다.

PowerMock을 사용하려면 위의 두 테스트 프레임워크 중 하나에 의존을 해야 합니다.

또한 PowerMock은 Junit 및 TestNG을 지원합니다.(둘 다 자바의 유닛 테스트 프레임워크입니다.)

build.gradle에 dependency 추가하기

 repositories {
        mavenCentral()
    }

dependencies {
    testImplementation group: 'junit', name: 'junit', version: '4.12'
    testImplementation 'org.powermock:powermock-module-junit4:2.0.4'
    testImplementation 'org.powermock:powermock-api-mockito2:2.0.4'
}

아래 링크의 지원되는 버전을 확인하고 dependency를 추가합니다(글 작성 시점 PowerMock 2.0.4 최신입니다)

PowerMock Support Version 확인하기

캡슐화 우회하기

WhiteBox Class는 필요한 경우 캡슐화를 우회하는데 도움이 되는 메서드들을 제공합니다.

일반적으로는 private 필드를 가져오거나 수정하는 것은 좋지 않지만

때로는 리펙토링을 텍스트 하여 코드를 다루는 방법일 수도 있습니다.

  1. Whitebox.setInternalState(..) 인스턴스 또는 클래스의 비공개 멤버를 설정하는 데사용합니다.
  2. Whitebox.getInternalState(..) 인스턴스 나 클래스의 비공개 멤버를 얻는 데사용합니다.
  3. Whitebox.invokeMethod(..) 인스턴스 또는 클래스의 비공개 메서드를 호출하는 데사용합니다.
  4. Whitebox.invokeConstructor(..) 개인 생성자로 클래스의 인스턴스를 만드는 데사용합니다.

WhiteBox 클래스는 org.powermock.reflect.WhiteBox 패키지에 정의되어 있으며

패키지 명에서 알 수 있듯이 모든 기능은 reflection으로 만들어져 있습니다.

유저들이 직접 Reflection을 이용하여 구현하는 수고를 덜어주는 게 바로 PowerMock인 것입니다.

1. private field에 접근하기

데모의 목적으로 아래와 같은 클래스가 있다고 가정해보도록 하겠습니다.

class ServiceHolder {

    private val services = HashSet<Any>()

    fun addService(service: Any) {
        services.add(service)
    }

    fun removeService(service: Any) {
        services.remove(service)
    }
}

addService 메서드가 호출된 후의 상태가 올바르게 업데이트되었는지 테스트하고 싶다고 하겠습니다.

즉 ServiceHolder에는 새로운 객체가 HashSet에 추가된 것입니다.

이를 수행하는 한 가지 방법은 getService()를 반환하는 메서드를 추가하는 것입니다.

하지만 이렇게 함으로써 클래스를 테스트할 수 있게 만드는 것 외에 다른 목적이 없는 메서드를 ServiceHolder에 추가하게 되는 것입니다.

이러한 문제를 PowerMock의 Whitebox.getInternalState(..)를 사용하여

기존 코드를 변경하지 않고 테스트할 수 있습니다.

public static <T> T getInternalState(Object object, String fieldName) {
        return WhiteboxImpl.getInternalState(object, fieldName);
    }

아래와 같이 테스트합니다.

 @Test
    @Throws(Exception::class)
    fun testAddService() {
        val tested = ServiceHolder()
        val service = Any()

        tested.addService(service)

        // This is how you get the private services set using PowerMock
        val services = Whitebox.getInternalState<Set<String>>(
            tested,
            "services"
        )

        Assert.assertEquals(
            "Size of the \"services\" Set should be 1", 1, services
                .size
        )
        Assert.assertSame(
            "The services Set should didn't contain the expect service",
            service, services.iterator().next()
        )
    }

assertEquals는 두 값이 같은지를 비교하는 단정 문이고, assertSame은 두 객체가 동일한 객체인지 주소 값으로 비교하는 assertion입니다.

2. private field 값 변경하기

PowerMock을 이용하면 객체의 private 변숫값을 얻을 수 있을 뿐 아니라 변경도 가능합니다.

아래의 예로 확인해보겠습니다.

class ReportGenerator {

    private val reportTemplateService: ReportTemplateService? = null

    fun generateReport(reportId: String): Report {
        val templateId = reportTemplateService?.getTemplateId(reportId)
        /*
         * Imagine some other code here that generates the report based on the
         * template id.
         */
        return Report("name")
    }
}

ReportGenerator 클래스의 내부에는 private로 선언된 reportTemplateService가 있습니다.

좋은 구조는 아니지만 예를 들어보기 위해 reportTemplateService는 nullable로 선언되어있고 초기화되어있지 않습니다.

또한 접근 제한자가 private 이기 때문에 프로퍼티에 값을 set 할 수 없는 구조라 하겠습니다.

이러한 구조라 하여도 PowerMock을 이용하면 값을 set 해줄 수 있습니다.

WhiteBox.setInternalState() 함수

public static void setInternalState(Object object, String fieldName, Object value) {
        WhiteboxImpl.setInternalState(object, fieldName, value);
    }
  @Test
    fun reportTemplateServiceAddTest() {
        val reportGenerator = ReportGenerator()
        val reportTemplate = ReportTemplateService()

        Whitebox.setInternalState(reportGenerator, "reportTemplateService", reportTemplate)

        Assert.assertSame(
            "The report template didn't expect template",
            reportTemplate,
            Whitebox.getInternalState<ReportTemplateService>(
                reportGenerator,
                "reportTemplateService"
            )
        )
    }

Whitebox.setInternalState() 함수를 이용하여 ReportGenerator의 private변수 reportTemplateService를 초기화한 뒤

잘 초기화되었는지 테스트하는 내용입니다.

3. private method 호출하기(invocation)

PowerMock을 이용하여 private 접근자로 선언된 메서드를 호출할 수도 있습니다. invokeMethod() 함수를 이용합니다.

invokeMethod() 함수

public static synchronized <T> T invokeMethod(Object instance, String methodToExecute, Object... arguments)
            throws Exception {
        return WhiteboxImpl.invokeMethod(instance, methodToExecute, arguments);
    }

예시를 보도록 하겠습니다 아래와 같은 간단한 덧 샘만 가능한 Calculator 클래스가 있습니다.

class Calculator {
    private fun sum(a: Int, b: Int) = a + b
    private fun sub(a: Int, b: Int) = a - b
}

아래와 같이 호출하여 테스트합니다.

    @Test
    fun calculatorSumMethodCallTest() {
        val calculator = Calculator()

        val result = Whitebox.invokeMethod<Int>(calculator, "sum", 1, 2)

        Assert.assertEquals(3, result)
    }

invokeMethod에 Generic으로 넘겨주는 값은 호출될 함수의 return값 타입을 넘겨주어야 합니다.

invokeMethod를 사용할 때 주의해야 할 점이 있습니다.

invokeMethod( )는 메서드 이름, 메서드 파라미터 타입으로 메서드를 찾습니다.

      val result = Whitebox.invokeMethod<Int>(calculator, 1, 2)

위와 같이 함수의 이름 없이 호출한다면 아래와 같은 error를 만날 수 있을 것입니다.

org.powermock.reflect.exceptions.TooManyMethodsFoundException: Several matching methods found, please specify the argument parameter types so that PowerMock can determine which method you're referring to.
Matching methods in class k.bs.powermock.demo.calculator.Calculator were:
int sum( int.class int.class )
int sub( int.class int.class )

같은 파라미터 타입을 가진 메서드가 있다면 호출이 불가능하므로 함수 이름까지 꼭 매개변수로 넘겨주는 편이 좋습니다.

4. private constructor invocation (private 생성자 호출 )

PowerMock을 이용하여 private생성자를 직접 호출하는 것도 가능합니다.

invokeConstructor() 메서드를 이용합니다.

아래와 같은 private construct를 가지는 클래스가 있습니다.

class PrivateConstructorInstantiation
private constructor(val state: Int = 2)

invokeConstructor()

public static <T> T invokeConstructor(Class<T> classThatContainsTheConstructorToTest, Object... arguments)
        throws Exception {
    return WhiteboxImpl.invokeConstructor(classThatContainsTheConstructorToTest, arguments);
}

invokeConstructor() 함수를 이용하여 private생성자를 호출하는 간단한 테스트입니다.

    @Test
    fun privateConstructorCallTest() {
        val privateConstructorInstantiation =
            Whitebox.invokeConstructor(PrivateConstructorInstantiation::class.java, 1)

        Assert.assertEquals(1, privateConstructorInstantiation.state)
    }

원하지 않는 행동 억제하기()

1. 부모 클래스의(super class) 생성자 억제하기

PowerMock을 이용하여 부모 클래스의 생성자를 호출하지 않도록 할 수 있습니다.

아래와 같은 부모 클래스가 있습니다

부모 클래스의 생성자에서 library를 load 한다고 해봅시다.

open class EvilParent {
    init {
        println("loadLibrary(evil.dll)")
    }
}

아래는 EvilParent를 상속받는 자식 클래스입니다.

class ExampleWithEvilParent( val message: String="") : EvilParent()

만약 자식 클래스를 객체를 생성하면서

라이브러리를 로드하는 부모 클래스의 생성자는 호출하고 싶지 않다면 어떻게 해야 할까요?

PowerMock의 suppress(constructor(..))를 사용하면 됩니다.

@Test
    fun `suppress super class constructor test`() {
        suppress(MemberMatcher.constructor(EvilParent::class.java))

        ExampleWithEvilParent()
    }

위와 같은 방법으로 부모 클래스의 생성자를 억제합니다.

suppress(constructor(ClassWithSeveralConstructors.class, String.class));

만약 생성자가 여러 개여서 특정 생성자만 억제하고 싶다면

위처럼 억제할 클래스와 억제할 생성자의 파라미터 타입을 같이 넣어주면 특정 생성자만 억제 가능합니다.

*suppress() 메서드를 이용하여 생성자를 억제하려면 *

클래스 레벨에서 아래와 같이 어노테이션을 꼭 작성해주어야 합니다.

@RunWith(PowerMockRunner::class)
@PrepareForTest(ExampleWithEvilParent::class)
class SuppressUnwantedBehavior {
...

}

2. 자신의 생성자 억제하기

PowerMock은 생성자를 호출하지 않도록 억제할 수 있습니다.

예를 들어 자신의 코드가 생성자에서 무언가를 수행하여 단위 테스트를 어렵게 할 경우

생성자를 호출하지 않고 클래스를 만들 수 있는 것입니다.

아래와 같은 클래스가 있습니다.

class ExampleWithEvilConstructor(val message: String) {

    init {
        println("System.loadLibrary(\"evil.dll\")")
    }
}

아래와 같이 Whitebox.newInstance() 메서드를 이용하여 생성자를 호출하지 않고 인스턴스를 생성합니다.

@Test
fun `suppress own constructor`() {
val tested = Whitebox.newInstance(ExampleWithEvilConstructor::class.java)
Assert.assertNull(tested.message)
}

@RunWith(..)이나 @PrepareForTest에 클래스를 전달할 필요는 없습니다.

3. Method 억제

특별한 경우에는 어떤 메서드를 호출하였을 때 기본값을 리턴하도록 해야 하는 경우가 있을 것입니다.

다른 경우에는 단위 테스트를 하지 못하게 하는 메서드 수행을 억제하거나 Mock 해야 하는 경우도 있을 수 있습니다.

바로 예시를 보도록 하겠습니다.

class ExampleWithEvilMethod(private val message: String) {

    fun getMessage(): String {
        return "$message${getEvilMessage() ?: ""}"
    }

    private fun getEvilMessage(): String? {
        println("System.loadLibrary(\"evil.dll\")")
        return "evil!"
    }
}

getMessage() 함수의 내부에서 getEvilMessage()를 호출합니다.

여기서 getEvilMessage을 무시하고 싶다면 아래와 같이 작업을 할 수 있습니다.

@RunWith(PowerMockRunner::class)
@PrepareForTest(ExampleWithEvilMethod::class)
class SuppressUnwantedBehavior {

    @Test
    fun `suppress method test`() {
        suppress(method(ExampleWithEvilMethod::class.java, "getEvilMessage"))
        val message = "my message."
        val tested = ExampleWithEvilMethod(message)

        Assert.assertEquals(message, tested.getMessage())
    }
}

PowerMock의 suppress(method(..))을 이용합니다.

suppress(MemberMatcher.method(ExampleWithEvilMethod::class.java, "getEvilMessage"))

클래스와, 메서드 이름을 파라미터로 넣어줍니다.

*클래스 레벨에서 @RunWith(PowerMockRunner::class)과 @PrepareForTest(ExampleWithEvilMethod::class)를 *

반드시 호출해주어야 합니다.

5. fields 억제하기

PowerMock의 필드 억제를 사용하여 필드를 표시하지 않을 수도 있습니다.

아래와 같은 필드를 가지는 클래스가 있습니다.

class MyClass {
    val myObject = MyObject()
}

myObject를 억제하고 싶다면 아래와 같이 호출하면 됩니다.

@RunWith(PowerMockRunner::class)
@PrepareForTest(MyClass::class)
class SuppressUnwantedBehavior {
    @Test
    fun `suppress field test`() {
        suppress(MemberMatcher.fields(MyClass::class.java, "myObject"))
        val myClass = MyClass()

        Assert.assertNull(myClass.myObject)
    }
}

아래와 같은 식으로 필드를 억제합니다.

suppress(field(MyClass.class, "myObject"))

억제된 field를 호출하면 null을 리턴합니다.

Suppress Unwanted Behavior 빠른 요약

  1. @RunWith(PowerMockRunner.class) 어노테이션을 클래스 레벨에서 사용해야 합니다.
  2. @PrepareForTest(ClassWithEvilParentConstructor.class)과 조합하여 테스트 케이스의 클래스 수준에서 어노테이션을 작성하고, 테스트 메서드 안에서 suppress(constructor(EvilParent.class))를 호출하여 부모 클래스의 생성자를 억제할 수 있습니다.
  3. Whitebox.newInstance(ClassWithEvilConstructor.class)메서드를사용하여생성자를 호출하지 않고 클래스를 인스턴스화 할 수 있습니다.
  4. ClassWithEvilMethod 클래스에서 이름이 "methodName"인 메서드를 억제하려면@PrepareForTest(ClassWithEvilMethod.class)어노테이션을 클래스레벨에 작성하고 , 메소드 내부에서 suppress(method(ClassWithEvilMethod.class, "methodName"))을 사용
  5. ClassWithEvilField 클래스에서 이름이 "fieldName"인 필드를 억제하려면@PrepareForTest(ClassWithEvilField.class) 어노테이션을 클래스 레벨에서 작성, 메서드 내부에서 suppress(field(ClassWithEvilField.class, "fieldName"))을 사용

멤버 수정 및 멤버 매처 메서드는 다음에서 찾을 수 있습니다.

  • org.powermock.api.support.membermodification.MemberModifier
  • org.powermock.api.support.membermodification.MemberMatcher

 

 


 

 

 

 

PowerMock with Mockito 알아보기

 

1.  static method mocking

정적 메서드를 mock 및 sutb하는 방법에대하여 알아보겠습니다.

1-1 클래스 레벨에서 @PrepareForTest(..) 어노테이션을 추가합니다.

@PrepareForTest(Static.class) // Static.class contains static methods

1-2 테스트 메소드 내부에서 PowerMockito.mockStatic() 메소드를 호출합니다.

PowerMockito.mockStatic(Static.class);

1.3 기존 모키토처럼 when 등을 이용하여 stub 합니다.

Mockito.when(Static.firstStaticMethod(param)).thenReturn(value);

*2-1 Static method verify 하기 *

static 메서드를 verification 하려면 두 단계가 필요합니다.

  1. PowerMockito.verifyStatic(Static.class) 호출
  2. staticmethod 호출

기존의 Mockito의 verify와 순서가 반대로 입니다.

예를 보고 알아보겠습니다.

PowerMockito.verifyStatic(Static.class) // 1
Static.firstStaticMethod(param) // 2

3-1 Static method argument matcher 사용하기

Mocktio의 arguemtMatcher는 똑같이 staticMethod에서도 적용될 수 있습니다.

아래와 같이 사용합니다.

PowerMockito.verifyStatic(Static.class)
Static.thirdStaticMethod(Mockito.anyInt())

4-1 정확한 호출수 확인하기

여전히 Mockito.VerificationMode (예 : Mockito.times (x))를

PowerMockito.verifyStatic(Static.class, Mockito.times(2))다음과 함께 사용할 수 있습니다.

PowerMockito.verifyStatic(Static.class, Mockito.times(1))

5-1 void 정적 메서드를 stub 하여 exception을 반환하는 방법

 

private가 아닌 경우

PowerMockito.doThrow(ArrayStoreException("Mock error")).when(StaticService.class)
StaticService.executeMethod()

final class / method에 대해서도 동일한 작업을 수행 가능합니다.

PowerMockito.doThrow(ArrayStoreException("Mock error")).when(myFinalMock).myFinalMethod()

private인 경우 아래와 같이 사용합니다.

when(tested, "methodToExpect", argument).thenReturn(myReturnValue)

6-1 example

object DemoStaticClass {
    @JvmStatic
    fun firstStaticMethod(param: Int): Int = param * 2

    @JvmStatic
    fun secondStaticMethod(): Int = 2

    @JvmStatic
    fun thirdStaticMethod() {

    }
}
@RunWith(PowerMockRunner::class)
@PrepareForTest(DemoStaticClass::class)
class YourTestCase {

    @Before
    fun init() {
        PowerMockito.mockStatic(DemoStaticClass::class.java)
    }

    @Test
    fun testMethodThatCallsStaticMethod() {
        Mockito.`when`(DemoStaticClass.firstStaticMethod(2)).thenReturn(4)
        Mockito.`when`(DemoStaticClass.secondStaticMethod()).thenReturn(123)


        DemoStaticClass.firstStaticMethod(2)
        DemoStaticClass.firstStaticMethod(2)
        PowerMockito.verifyStatic<DemoStaticClass>(DemoStaticClass::class.java, Mockito.times(2))
        DemoStaticClass.firstStaticMethod(2)
        DemoStaticClass.firstStaticMethod(2)


        DemoStaticClass.secondStaticMethod()
        PowerMockito.verifyStatic<DemoStaticClass>(DemoStaticClass::class.java) // default times is once
        DemoStaticClass.secondStaticMethod()


        PowerMockito.verifyStatic<DemoStaticClass>(DemoStaticClass::class.java, Mockito.never())
        DemoStaticClass.thirdStaticMethod()
    }
}

 

 

2. Partial mocking(부분 mock) 하기

PowerMockito를 사용하여 PowerMockito.spy를 사용하여 메서드를 부분적으로 mock 할 수 있습니다.

   @Test
    fun `partial mocking test`() {
        val spy = PowerMockito.spy(LinkedList<String>())

        //exception occurred
//        `when`(spy[0]).thenReturn("foo")

        doReturn("foo").`when`(spy)[0]

        spy[0]

        verify(spy)[0]
    }

 

 

3. verify private behavior

PowerMockito.verifyPrivate()를 사용하여 

접근 제한자 private인 내용에 대해서도 verify 가능합니다.

verifyPrivate(tested).invoke("privateMethodName", argument1)

 

 

4. 새로운 객체를 생성을 mock 하는 방법

whenNew(MyClass::class.java).withNoArguments().thenThrow(IOException("error message"))