programing

Mockito를 사용한 정적 메서드 조롱

nicescript 2022. 9. 30. 13:19
반응형

Mockito를 사용한 정적 메서드 조롱

나는 생산하기 위해 공장을 썼다.java.sql.Connection★★★★★★★★★★★★★★★★★★:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return DriverManager.getConnection(...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

DriverManager.getConnection하지만 정적인 방법을 조롱하는 방법은 모릅니다.JUnit4 모키토이 특정 사용 사례를 조롱/검증할 수 있는 좋은 방법이 있습니까?

Mockito 위에 Power Mockito를 얹어 사용합니다.

코드 예:

@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {

    @Test
    public void shouldVerifyParameters() throws Exception {

        //given
        PowerMockito.mockStatic(DriverManager.class);
        BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);

        //when
        sut.execute(); // System Under Test (sut)

        //then
        PowerMockito.verifyStatic();
        DriverManager.getConnection(...);

    }

상세 정보:

사용하지 않을 수 없는 정적 메서드를 피하기 위한 일반적인 전략은 랩된 개체를 만들고 대신 래퍼 개체를 사용하는 것입니다.

래퍼 오브젝트는 실제 스태틱클래스의 패스가 되고 테스트하지 않습니다.

래퍼 오브젝트는 다음과 같습니다.

public class Slf4jMdcWrapper {
    public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();

    public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
        return MDC.getWhateverIWant();
    }
}

마지막으로 테스트 대상 클래스는 이 싱글톤 객체를 사용할 수 있습니다.예를 들어, 실생활용 디폴트 컨스트럭터가 있는 경우입니다.

public class SomeClassUnderTest {
    final Slf4jMdcWrapper myMockableObject;

    /** constructor used by CDI or whatever real life use case */
    public myClassUnderTestContructor() {
        this.myMockableObject = Slf4jMdcWrapper.SINGLETON;
    }

    /** constructor used in tests*/
    myClassUnderTestContructor(Slf4jMdcWrapper myMock) {
        this.myMockableObject = myMock;
    }
}

이 클래스는 쉽게 테스트할 수 있습니다.스태틱 메서드에서는 클래스를 직접 사용하지 않기 때문입니다.

CDI를 사용하고 있으며 @Inject 주석을 사용할 수 있다면 훨씬 더 쉽습니다.Wrapper bean을 @ApplicationScoped로 만들고 공동작업자로서 Wrapper bean을 주입하기만 하면 됩니다(테스트에 복잡한 컨스트럭터도 필요 없습니다).이렇게 조롱을 계속합니다.

Mockito 3.4.0 이후로는 Mockito에서 정적 메서드를 조롱할 수 있습니다.상세한 것에 대하여는, 다음을 참조해 주세요.

https://github.com/mockito/mockito/releases/tag/v3.4.0

https://github.com/mockito/mockito/issues/1013

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#static_mocks

assertEquals("foo", Foo.method());
try (MockedStatic mocked = mockStatic(Foo.class)) {
 mocked.when(Foo::method).thenReturn("bar");
 assertEquals("bar", Foo.method());
 mocked.verify(Foo::method);
}
assertEquals("foo", Foo.method());

당신의 경우, 다음과 같습니다.

  @Test
  public void testStaticMockWithVerification() throws SQLException {
    try (MockedStatic<DriverManager> dummy = Mockito.mockStatic(DriverManager.class)) {
      DatabaseConnectionFactory factory = new MySQLDatabaseConnectionFactory();
      dummy.when(() -> DriverManager.getConnection("arg1", "arg2", "arg3"))
        .thenReturn(new Connection() {/*...*/});

      factory.getConnection();

      dummy.verify(() -> DriverManager.getConnection(eq("arg1"), eq("arg2"), eq("arg3")));
    }
  }

메모: STATIC 메서드를 조롱하려면 mockito-core 대신 mockito-inline 의존성이 필요합니다.

JUnit5의 경우 다음 항목도 추가합니다.

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>${mockito.version}</version>
  <scope>test</scope>
</dependency>

저도 비슷한 문제가 있었어요. 「이러다」라고 까지, 않았다.@PrepareForTest(TheClassThatContainsStaticMethod.class)PowerMock의 mockStatic 매뉴얼에 따르면.

꼭 쓸 BDDMockito

내 클래스:

public class SmokeRouteBuilder {
    public static String smokeMessageId() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("Exception occurred while fetching localhost address", e);
            return UUID.randomUUID().toString();
        }
    }
}

나의 테스트 클래스:

@RunWith(PowerMockRunner.class)
@PrepareForTest(SmokeRouteBuilder.class)
public class SmokeRouteBuilderTest {
    @Test
    public void testSmokeMessageId_exception() throws UnknownHostException {
        UUID id = UUID.randomUUID();

        mockStatic(InetAddress.class);
        mockStatic(UUID.class);
        when(InetAddress.getLocalHost()).thenThrow(UnknownHostException.class);
        when(UUID.randomUUID()).thenReturn(id);

        assertEquals(id.toString(), SmokeRouteBuilder.smokeMessageId());
    }
}

앞에서 설명한 바와 같이 mockito를 사용하여 정적 메서드를 조롱할 수 없습니다.

테스트 프레임워크를 변경할 수 없는 경우 다음을 수행할 수 있습니다.

DriverManager용 인터페이스를 만들고, 이 인터페이스를 모의하고, 의존관계 주입을 통해 주입하고, 모의에서 확인합니다.

JUnit 5를 사용하는 사용자에게 Powermock은 선택사항이 아닙니다.Mockito만으로 정적 메서드를 성공적으로 모킹하려면 다음과 같은 종속성이 필요합니다.

testCompile    group: 'org.mockito', name: 'mockito-core',           version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-junit-jupiter',  version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-inline',         version: '3.6.0'

mockito-junit-jupiterJUnit 5는 JUnit 5입니다.

또한 정적 방법을 조롱하기 위한 지원은 다음과 같이 제공됩니다.mockito-inline★★★★★★ 。

예:

@Test
void returnUtilTest() {
    assertEquals("foo", UtilClass.staticMethod("foo"));

    try (MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)) {

        classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");

        assertEquals("bar", UtilClass.staticMethod("foo"));
     }

     assertEquals("foo", UtilClass.staticMethod("foo"));
}

리소스 사용 블록은 정적 모크를 임시로 유지하기 위해 사용되므로 해당 범위 내에서만 조롱됩니다.

Try Block을 사용하지 않을 때는 어설션이 끝나면 반드시 모크를 닫습니다.

MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)
classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");
assertEquals("bar", UtilClass.staticMethod("foo"));
classMock.close();

모의 보이드 방식:

mockStatic 메서드는 으로 '비활성화'로 됩니다.doNothing().

관찰 : 스태틱엔티티 내에서 스태틱메서드를 호출하는 경우 @PrepareForTest에서 클래스를 변경해야 합니다.

예:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

위의 코드에 대해서는 MessageDigest 클래스를 모킹해야 할 경우

@PrepareForTest(MessageDigest.class)

단, 다음과 같은 것이 있는 경우:

public class CustomObjectRule {

    object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
             .digest(message.getBytes(ENCODING)));

}

이 코드가 있는 클래스를 준비해야 합니다.

@PrepareForTest(CustomObjectRule.class)

그런 다음 메서드를 조롱합니다.

PowerMockito.mockStatic(MessageDigest.class);
PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
      .thenThrow(new RuntimeException());

Mockito와 Aspect J의 조합도 썼습니다.https://github.com/iirekm/varia/tree/develop/ajmock

다음은 예를 제시하겠습니다.

when(() -> DriverManager.getConnection(...)).thenReturn(...);

약간의 리팩터링을 통해 이를 수행할 수 있습니다.

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return _getConnection(...some params...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    //method to forward parameters, enabling mocking, extension, etc
    Connection _getConnection(...some params...) throws SQLException {
        return DriverManager.getConnection(...some params...);
    }
}

할 수 요.MySQLDatabaseConnectionFactory조롱된 연결을 반환하거나 매개 변수에 대한 어설션을 수행합니다.

확장 클래스가 같은 패키지에 있는 경우 테스트 케이스 내에 존재할 수 있습니다(권장).

public class MockedConnectionFactory extends MySQLDatabaseConnectionFactory {

    Connection _getConnection(...some params...) throws SQLException {
        if (some param != something) throw new InvalidParameterException();

        //consider mocking some methods with when(yourMock.something()).thenReturn(value)
        return Mockito.mock(Connection.class);
    }
}

Mockito는 정적 메서드를 캡처할 수 없지만 Mockito 2.14.0 이후 정적 메서드의 호출 인스턴스를 생성하여 시뮬레이션을 수행할 수 있습니다.

예(테스트에서 추출):

public class StaticMockingExperimentTest extends TestBase {

    Foo mock = Mockito.mock(Foo.class);
    MockHandler handler = Mockito.mockingDetails(mock).getMockHandler();
    Method staticMethod;
    InvocationFactory.RealMethodBehavior realMethod = new InvocationFactory.RealMethodBehavior() {
        @Override
        public Object call() throws Throwable {
            return null;
        }
    };

    @Before
    public void before() throws Throwable {
        staticMethod = Foo.class.getDeclaredMethod("staticMethod", String.class);
    }

    @Test
    public void verify_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        handler.handle(invocation);

        //verify staticMethod on mock
        //Mockito cannot capture static methods so we will simulate this scenario in 3 steps:
        //1. Call standard 'verify' method. Internally, it will add verificationMode to the thread local state.
        //  Effectively, we indicate to Mockito that right now we are about to verify a method call on this mock.
        verify(mock);
        //2. Create the invocation instance using the new public API
        //  Mockito cannot capture static methods but we can create an invocation instance of that static invocation
        Invocation verification = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "some arg");
        //3. Make Mockito handle the static method invocation
        //  Mockito will find verification mode in thread local state and will try verify the invocation
        handler.handle(verification);

        //verify zero times, method with different argument
        verify(mock, times(0));
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        handler.handle(differentArg);
    }

    @Test
    public void stubbing_static_method() throws Throwable {
        //register staticMethod call on mock
        Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "foo");
        handler.handle(invocation);

        //register stubbing
        when(null).thenReturn("hey");

        //validate stubbed return value
        assertEquals("hey", handler.handle(invocation));
        assertEquals("hey", handler.handle(invocation));

        //default null value is returned if invoked with different argument
        Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                "different arg");
        assertEquals(null, handler.handle(differentArg));
    }

    static class Foo {

        private final String arg;

        public Foo(String arg) {
            this.arg = arg;
        }

        public static String staticMethod(String arg) {
            return "";
        }

        @Override
        public String toString() {
            return "foo:" + arg;
        }
    }
}

그들의 목표는 스태틱 모킹(static morkito)을 직접 지원하는 것이 아니라 퍼블릭 API를 개선하여 Powermockito와 같은 다른 라이브러리가 내부 API에 의존하거나 일부 Mockito 코드를 직접 복제할 필요가 없도록 하는 것입니다.(소스)

면책사항:모키토 팀은 지옥으로 가는 길은 정적인 방법으로 포장되어 있다고 생각한다.그러나 Mockito의 일은 정적 메서드로부터 코드를 보호하는 것이 아닙니다.스태틱한 조롱을 하는 팀이 싫으면 조직에서 Powermockito 사용을 중지하십시오.Mockito는 Java 테스트를 어떻게 작성해야 하는지에 대한 독단적인 비전을 가진 툴킷으로 진화해야 합니다(예를 들어 통계학을 조롱하지 마십시오!!).그러나 모키토는 독단적이지 않다.정적 조롱과 같은 권장되지 않는 사용 사례를 차단하고 싶지 않습니다.그건 우리 일이 아니야.

정적 방식을 시뮬레이션하려면 https://github.com/powermock/powermock/wiki/MockStatic에서 Powermock을 참조해야 합니다.Mockito는 이 기능을 제공하지 않습니다.

모키토에 관한 멋진 기사를 읽을 수 있습니다.http://refcardz.dzone.com/refcardz/mockito

모키토에서 해결책을 하나 찾았어요이 기능은 다음 버전에서만 제공됩니다.3.4.0

https://asolntsev.github.io/en/2020/07/11/mockito-static-methods/

  • 의존

    build.gradle에서 mockito-core: 3.3.3을 mockito-inline: 3.4.0으로 바꿉니다.

    testImplementation('org.mockito:mockito-inline:3.4.0')
    
  • 무엇을 조롱할 것인가?

     class Buddy 
     {
         static String name() 
         {
            return "John";
         }
     }
    
  • 정적 메서드를 조롱합니다.

        @Test
        void lookMomICanMockStaticMethods() 
        {
             assertThat(Buddy.name()).isEqualTo("John");
    
            try (MockedStatic<Buddy> theMock = Mockito.mockStatic(Buddy.class)) 
            {
                theMock.when(Buddy::name).thenReturn("Rafael");
                assertThat(Buddy.name()).isEqualTo("Rafael");
            }
    
            assertThat(Buddy.name()).isEqualTo("John");
        }
    

이게 도움이 될 것 같아

그 방법은 정적이기 때문에 사용하는 데 필요한 것은 모두 갖추어져 있기 때문에 조롱의 목적은 없어집니다.정적 방식을 조롱하는 것은 잘못된 관행으로 간주됩니다.

그렇게 하려고 하면 테스트 수행 방법에 문제가 있다는 것을 의미합니다.

물론 Power Mockito나 이를 가능하게 하는 다른 프레임워크도 사용할 수 있지만, 접근 방식을 재고해 보십시오.

예를 들어, 오브젝트를 모킹/제공하려고 합니다.그 대신 스태틱메서드로 소비합니다.

JMockit 프레임워크를 사용합니다.그것은 나에게 효과가 있었다.DBConection.getConnection() 메서드를 조롱하기 위한 문을 쓸 필요가 없습니다.아래 코드만 있으면 됩니다.

@아래의 Mock은 mockit입니다.모의 패키지

Connection jdbcConnection = Mockito.mock(Connection.class);

MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {

            DBConnection singleton = new DBConnection();

            @Mock
            public DBConnection getInstance() { 
                return singleton;
            }

            @Mock
            public Connection getConnection() {
                return jdbcConnection;
            }
         };

java functional을 사용하여 간단한 솔루션이 있습니다.인터페이스 후 유닛테스트를 시행하는 클래스의 의존관계로서 그 인터페이스를 추가합니다.

정적 기능을 조롱하기 위해 이 방법을 사용할 수 있었습니다.

  • 일부 도우미 클래스/객체에 래퍼 함수를 만듭니다.(바리안트라는 이름을 사용하는 것이 물건을 분리하여 유지보수가 가능하도록 하는 데 도움이 될 수 있습니다.
  • 이 래퍼를 코드에 사용합니다.(네, 코드는 테스트를 염두에 두고 구현해야 합니다.)
  • 래퍼 기능을 모킹합니다.

래퍼 코드 스니펫(실제로 기능하는 것은 아니고, 일러스트만을 위해서)

class myWrapperClass ...
    def myWrapperFunction (...) {
        return theOriginalFunction (...)
    }

물론 단일 래퍼 클래스에 이러한 기능이 여러 개 축적되는 것은 코드 재사용 측면에서 유익할 수 있습니다.

여기에서는 Leokom의 솔루션에 대한 답변에서 약속한 대로 확장을 기반으로 한 Mockito MockStatic 솔루션을 소개합니다.

그렇다면, 왜 Mockito는 자원을 사용한 트라이를 선택했을까요?음, 단지 그들이 깔끔한 배를 유지하길 원하기 때문이지.그것은 역시 좋은 프로그래밍입니다.자원을 사용하여 종료 메서드를 확실하게 호출할 수 있습니다.하지만 JUnit에서는 이미 BeforeEach와 AfterEach에 그것이 있습니다.또한 BeforeEachCallback 및 AfterEachCallback을 구현하는 Extension을 사용하여 각 테스트클래스에 범용 목적으로 쉽게 추가할 수 있습니다.

이론은 여기까지입니다.정적인 모의고사를 만들어 봅시다.

Instant.now()

먼저 주석을 달아 테스트 클래스의 필드를 스태틱모크로 사용할 수 있도록 했습니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface StaticMock {

}

이를 통해 Extension 클래스에서 쉽게 찾을 수 있는 정적 조롱 필드를 테스트 클래스에 만들 수 있습니다.

  @StaticMock
  private MockedStatic<Instant> staticInstantMock;

작성한 Extension을 테스트 클래스에 추가했습니다.두 가지 옵션이 있습니다.

  1. 이 목적으로 Extension을 생성하여 Mockito Extension 옆에 있는 클래스에 추가합니다.이 클래스도 필요합니다.
  2. Extension을 생성하여 Mockito Extension에서 상속합니다.이제 테스트 클래스에서 Mockito Extension을 대체할 수 있습니다.

나는 둘 중 후자를 사용했다.

@ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {

이제 스태틱에 대해 반환할 무언가가 필요합니다.

  @Mock
  private Instant now;

  staticInstantMock.when(Instant::now).thenReturn(now);

전체 테스트 클래스:

@ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {

  @StaticMock
  private MockedStatic<Instant> staticInstantMock;

  @Mock
  private Instant now;

  @Test
  void myTestMethod() {
    staticInstantMock.when(Instant::now).thenReturn(now);

    assertThat(Instant::now).isSameAs(now); // This would normally happen in the class you are testing...
  }
}

이제 확장 클래스를 살펴보겠습니다.

import static org.mockito.Mockito.mockStatic;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

public class CompanyMockitoExtension extends MockitoExtension {

  @Override
  public void beforeEach(ExtensionContext context) {
    super.beforeEach(context); // Don't forget to call the super!!
    if (context.getTestInstance().isEmpty()) { // Just to be sure...
      return;
    }
    // Get the unit test instance
    Object testSubject = context.getTestInstance().get();
    initializeStaticMocks(testSubject);
  }

  private void initializeStaticMocks(Object testSubject) {
    // Find all fields that I want to static mock
    List<Field> staticMockFields = ReflectionHelper.getFieldsWithAnnotation(testSubject, StaticMock.class);
    staticMockFields.forEach(field -> initializeStaticMock(field, testSubject));
  }

  private void initializeStaticMock(Field field, Object testSubject) {
    // Get the type of the static mock. It is within the generic MockedStatic<> class type.
    Class<?> typeForStaticMock = (Class<?>) ReflectionHelper.getTypesForGeneric(field)[0];
    try {
      // Now set the field with the mockStatic method of Mockito.
      field.setAccessible(true);
      field.set(testSubject, mockStatic(typeForStaticMock));
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Failed to instantiate Static Mock with type: " + typeForStaticMock.getName());
    }
  }

  @Override
  public void afterEach(ExtensionContext context) {
    super.afterEach(context); // Again, do not forget to call the super.
    if (context.getTestInstance().isEmpty()) {
      return;
    }
    Object testSubject = context.getTestInstance().get();
    closeStaticMocks(testSubject); // Close all static mocks.
  }

  private void closeStaticMocks(Object testSubject) {
    // Again find all fields we annotated
    List<Field> staticMockFields = ReflectionHelper.getFieldsWithAnnotation(testSubject, StaticMock.class);
    staticMockFields.forEach(field -> closeStaticMock(field, testSubject));
  }

  private void closeStaticMock(Field field, Object testSubject) {
    // Get the instance and simply call close.
    MockedStatic<?> mockedStaticInstance = ReflectionHelper.getFieldInstance(field, testSubject, MockedStatic.class);
    mockedStaticInstance.close();
  }
}

이 확장의 좋은 점은 조롱을 추가할 수 있다는 것입니다.AfterEach의 모든 mock에 대해 더 이상 상호작용하지 않는다는 검증을 추가했습니다.이 확장자를 사용하면 자동으로 됩니다.건설의 조롱에도 정적 조롱과 비슷한 행동을 추가했습니다.

보다시피, 나는 나만의 반사 도우미 클래스를 만들었다.표준 성찰 도우미 클래스가 몇 개 있는데 그게 더 나을 수도 있어요.여기 이 목적을 위한 제 것이 있습니다.

public class ReflectionHelper {

  public static List<Field> getFieldsWithAnnotation(
      Object testSubject,
      Class<? extends Annotation> annotationType
  ) {
    Class<?> testSubjectClass = testSubject.getClass();

    return Arrays.stream(testSubjectClass.getDeclaredFields())
                 .filter(field -> field.isAnnotationPresent(annotationType))
                 .collect(toUnmodifiableList());
  }

  public static List<Field> getCollectionFields(Object testSubject) {
    Class<?> testSubjectClass = testSubject.getClass();

    return Arrays.stream(testSubjectClass.getDeclaredFields())
                 .filter(field -> Collection.class.isAssignableFrom(field.getType()))
                 .collect(toUnmodifiableList());
  }

  @SuppressWarnings("unchecked")
  public static <T> T getFieldInstance(Field field, Object testSubject, Class<T> type) {
    return (T) getFieldInstance(field, testSubject);
  }

  public static Object getFieldInstance(Field field, Object testSubject) {
    try {
      boolean isStatic = isStatic(field.getModifiers());
      Object context = isStatic ? null : testSubject;
      field.setAccessible(true);
      return field.get(context);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Failed to get instance of field.");
    }
  }

  public static Type[] getTypesForGeneric(Field field) {
    ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
    return parameterizedType.getActualTypeArguments();
  }
}

언급URL : https://stackoverflow.com/questions/21105403/mocking-static-methods-with-mockito

반응형