簡體   English   中英

Mocking static 方法與 Mockito

[英]Mocking static methods with 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的參數,但我不知道如何模擬 static 方法。 我在測試用例中使用 JUnit 4 和 Mockito。 是否有模擬/驗證此特定用例的好方法?

在 Mockito 之上使用PowerMockito

示例代碼:

@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(...);

    }

更多信息:

從 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")));
    }
  }

注意:模擬靜態方法需要 mockito-inline 依賴而不是 mockito-core。

對於 JUnit5,還要添加:

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

避開您無法避免使用的靜態方法的典型策略是創建包裝對象並改用包裝對象。

包裝器對象成為真正的靜態類的外觀,您無需對其進行測試。

包裝器對象可能類似於

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,將該東西作為協作者注入(您甚至不需要凌亂的構造函數來進行測試),然后繼續進行模擬。

我有一個類似的問題。 根據PowerMock 的 mockStatic 文檔,在我做出更改之前,接受的答案對我不起作用: @PrepareForTest(TheClassThatContainsStaticMethod.class)

而且我不必使用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-jupiter添加對 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-with-resource 塊用於使靜態模擬保持臨時狀態,因此僅在該范圍內模擬。

不使用 try 塊時,請確保在完成斷言后關閉模擬。

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

模擬 void 方法:

當在一個類上調用mockStatic時,該類中的所有靜態 void 方法都會自動模擬為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 和 AspectJ 的組合: 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;
        }
    }
}

他們的目標不是直接支持靜態模擬,而是改進其公共 API,以便其他庫(如Powermockito )不必依賴內部 API 或直接復制一些 Mockito 代碼。 來源

免責聲明:Mockito 團隊認為通往地獄的道路是用靜態方法鋪就的。 但是,Mockito 的工作不是保護您的代碼免受靜態方法的影響。 如果您不喜歡您的團隊進行靜態模擬,請停止在您的組織中使用 Powermockito。 Mockito 需要發展為一個工具包,對應該如何編寫 Java 測試有一個固執的願景(例如,不要模擬靜態!!!)。 然而,Mockito 並不是教條主義的。 我們不想阻止不推薦的用例,例如靜態模擬。 這不是我們的工作。

要模擬靜態方法,您應該使用 Powermock 查看: https ://github.com/powermock/powermock/wiki/MockStatic。 Mockito 不提供此功能。

您可以閱讀一篇關於 mockito 的精彩文章: http ://refcardz.dzone.com/refcardz/mockito

我在 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"); }

我認為這可以幫助我們。

由於該方法是靜態的,因此它已經擁有您使用它所需的一切,因此它違背了模擬的目的。 模擬靜態方法被認為是一種不好的做法。

如果您嘗試這樣做,則意味着您執行測試的方式有問題。

當然,您可以使用 PowerMockito 或任何其他能夠做到這一點的框架,但請嘗試重新考慮您的方法。

例如:嘗試模擬/提供該靜態方法使用的對象。

當您嘗試模擬靜態方法時,您必須在 try 塊內編寫測試。 因為重要的是要注意作用域模擬必須由激活模擬的實體關閉。

      try (MockedStatic<Tester> tester = Mockito.mockStatic(Tester.class)) {
            tester.when(() -> Tester.testStatic("Testing..")).thenReturn(mock(ReturnObject.class));
    //Here you have to write the test cases
      }

在上面的例子中,我們必須模擬 Tester 類 testStatic 方法,輸入參數為“Testing...”。 在這里,該方法將返回一個 ReturnObject 類類型的對象。 因此,我們在像上面那樣鏈接時編寫 mockito。

不要忘記在 Gradle/maven 中添加以下依賴項

    testImplementation 'org.mockito:mockito-inline:4.3.1'

使用 JMockit 框架 它對我有用。 您不必為模擬 DBConenction.getConnection() 方法編寫語句。 只需下面的代碼就足夠了。

@Mock 下面是 mockit.Mock 包

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 FunctionalInterface 有一個簡單的解決方案,然后將該接口添加為您嘗試進行單元測試的類的依賴項。

對於模擬靜態函數,我可以這樣做:

  • 在一些幫助類/對象中創建一個包裝函數。 (使用名稱變體可能有利於保持事物的分離和可維護性。)
  • 在你的代碼中使用這個包裝器。 (是的,代碼需要在考慮到測試的情況下實現。)
  • 模擬包裝函數。

包裝器代碼片段(不是真正的功能,僅用於說明)

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

當然,在單個包裝類中累積多個此類函數可能有利於代碼重用。

在這里,我根據我對 leokom 解決方案的回答中承諾的擴展來分享我的 mockito MockStatic 解決方案。

那么,為什么 Mockito 選擇 try-with-resources 呢? 好吧,僅僅是因為他們想保持一艘整潔的船。 畢竟這是一個很好的編程。 Try-with-resources 允許在保證調用 close 方法的情況下進行構造。 但是在 JUnit 中,我們已經在 BeforeEach 和 AfterEach 中擁有了它。 並且可以使用實現 BeforeEachCallback 和 AfterEachCallback 的擴展輕松地將這些用於通用目的添加到每個測試類。

理論就這么多。 讓我們為

Instant.now()

我從一個注釋開始,以便能夠在我的測試類中標記我想用作靜態模擬的字段。

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

}

這允許我在我的測試類中創建一個用於靜態模擬的字段,我可以在我的擴展類中輕松找到它。

  @StaticMock
  private MockedStatic<Instant> staticInstantMock;

我將我創建的擴展添加到我的測試類中。 你有兩個選擇。

  1. 為此目的創建一個 Extension 並將其添加到您還需要的 MockitoExtension 旁邊的類中。
  2. 創建一個擴展並讓它從 MockitoExtension 繼承。 現在你可以在你的測試類上替換 MockitoExtension。

我使用了兩者中的后者。

@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...
  }
}

現在讓我們看一下 Extension 類。

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 中的所有模擬上添加了不再交互的驗證。 現在,當我們使用此擴展程序時,這是自動的。 我還為構造模擬添加了與靜態模擬類似的行為。

如您所見,我創建了自己的反射助手類。 我知道有一些標准的反射助手類,這些可能會更好。 這是我的目的。

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

稍微重構一下:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {
    ConnectionSupplier connectionSupplier = () -> SupplierDriverManager.getConnection();

    public void setConnSupplier(ConnectionSupplier supplier) {
        this.connectionSupplier = supplier;
    }

    @Override 
    public Connection getConnection() {
        try {
            return connectionSupplier.conn();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    @FunctionalInterface
    interface ConnectionSupplier {
        Connection conn();
    }
}

然后你可以使用mockito

MySQLDatabaseConnectionFactory.ConnectionSupplier connectionSupplier = mock(MySQLDatabaseConnectionFactory.ConnectionSupplier.class);
when(connectionSupplier.conn()).thenReturn(yourMockObject);
yourConnectionFactory.setConnSupplier(connectionSupplier);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM